├── src ├── main │ ├── resources │ │ ├── application.properties │ │ └── application.yml │ └── java │ │ └── com │ │ └── amigoscode │ │ └── group │ │ └── ebankingsuite │ │ ├── account │ │ ├── Tier.java │ │ ├── AccountStatus.java │ │ ├── request │ │ │ └── AccountTransactionPinUpdateModel.java │ │ ├── response │ │ │ └── AccountOverviewResponse.java │ │ ├── AccountRepository.java │ │ ├── Account.java │ │ ├── AccountController.java │ │ └── AccountService.java │ │ ├── transaction │ │ ├── TransactionStatus.java │ │ ├── response │ │ │ ├── TransactionType.java │ │ │ └── TransactionHistoryResponse.java │ │ ├── request │ │ │ ├── FundsTransferRequest.java │ │ │ └── TransactionHistoryRequest.java │ │ ├── TransactionRepository.java │ │ ├── Transaction.java │ │ ├── TransactionController.java │ │ └── TransactionService.java │ │ ├── notification │ │ ├── smsNotification │ │ │ ├── SmsSenderService.java │ │ │ ├── SmsNotification.java │ │ │ └── twilio │ │ │ │ ├── TwilioConfiguration.java │ │ │ │ ├── TwilioInitializer.java │ │ │ │ └── TwilioSmsSenderService.java │ │ ├── emailNotification │ │ │ ├── EmailSenderService.java │ │ │ ├── EmailNotification.java │ │ │ ├── request │ │ │ │ └── FundsAlertNotificationRequest.java │ │ │ └── gmail │ │ │ │ └── GmailEmailSenderService.java │ │ ├── CreditDebitSmsAlertTemplate.java │ │ ├── CreditDebitEmailAlertTemplate.java │ │ └── NotificationSenderService.java │ │ ├── exception │ │ ├── ResourceExistsException.java │ │ ├── ValueMismatchException.java │ │ ├── AccountNotClearedException.java │ │ ├── AccountNotActivatedException.java │ │ ├── InsufficientBalanceException.java │ │ ├── ResourceNotFoundException.java │ │ ├── InvalidAuthorizationException.java │ │ ├── InvalidAuthenticationException.java │ │ └── CustomExceptionHandler.java │ │ ├── user │ │ ├── requests │ │ │ ├── ChangePasswordRequest.java │ │ │ ├── UserAuthenticationRequests.java │ │ │ └── UserRegistrationRequest.java │ │ ├── UserRepository.java │ │ ├── UserController.java │ │ ├── User.java │ │ └── UserService.java │ │ ├── universal │ │ └── ApiResponse.java │ │ ├── EBankingSuiteApplication.java │ │ └── config │ │ ├── SecurityConfiguration.java │ │ ├── ApplicationConfig.java │ │ ├── JWTFilter.java │ │ └── JWTService.java └── test │ ├── java │ └── com │ │ └── amigoscode │ │ └── group │ │ └── ebankingsuite │ │ ├── EBankingSuiteApplicationTests.java │ │ ├── transaction │ │ ├── TransactionRepositoryTest.java │ │ ├── TransactionControllerTest.java │ │ └── TransactionServiceTest.java │ │ ├── user │ │ ├── UserRepositoryTest.java │ │ ├── UserControllerTest.java │ │ └── UserServiceTest.java │ │ └── account │ │ ├── AccountRepositoryTest.java │ │ ├── AccountControllerTest.java │ │ └── AccountServiceTest.java │ └── resources │ └── application.yml ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .gitignore ├── docker-compose.yml ├── pom.xml ├── mvnw.cmd └── mvnw /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amigoscode/group-project-e-banking-suite/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/Tier.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | public enum Tier { 4 | LEVEL1, 5 | LEVEL2, 6 | LEVEL3 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/AccountStatus.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | public enum AccountStatus { 4 | ACTIVATED, 5 | BLOCKED, 6 | PENDING, 7 | CLOSED 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/TransactionStatus.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | public enum TransactionStatus { 4 | PENDING, 5 | FAIL, 6 | SUCCESS 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/response/TransactionType.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction.response; 2 | 3 | public enum TransactionType { 4 | 5 | CREDIT, 6 | DEBIT 7 | } 8 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/smsNotification/SmsSenderService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.smsNotification; 2 | 3 | public interface SmsSenderService { 4 | void sendSms(SmsNotification smsNotification); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/smsNotification/SmsNotification.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.smsNotification; 2 | 3 | public record SmsNotification( 4 | String receiverPhoneNumber, 5 | String message 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/ResourceExistsException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class ResourceExistsException extends RuntimeException{ 4 | public ResourceExistsException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/ValueMismatchException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class ValueMismatchException extends RuntimeException{ 4 | public ValueMismatchException(String message){ 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/AccountNotClearedException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class AccountNotClearedException extends RuntimeException{ 4 | public AccountNotClearedException(String message){ 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/AccountNotActivatedException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class AccountNotActivatedException extends RuntimeException{ 4 | public AccountNotActivatedException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/InsufficientBalanceException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class InsufficientBalanceException extends RuntimeException{ 4 | public InsufficientBalanceException(String message){ 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class ResourceNotFoundException extends RuntimeException{ 4 | 5 | public ResourceNotFoundException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/request/AccountTransactionPinUpdateModel.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account.request; 2 | 3 | import lombok.NonNull; 4 | 5 | public record AccountTransactionPinUpdateModel ( 6 | @NonNull 7 | String transactionPin 8 | ){ 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/EBankingSuiteApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class EBankingSuiteApplicationTests { 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/InvalidAuthorizationException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class InvalidAuthorizationException extends RuntimeException{ 4 | 5 | public InvalidAuthorizationException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/InvalidAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | public class InvalidAuthenticationException extends RuntimeException{ 4 | 5 | public InvalidAuthenticationException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/requests/ChangePasswordRequest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user.requests; 2 | 3 | import lombok.NonNull; 4 | 5 | public record ChangePasswordRequest ( 6 | @NonNull 7 | String oldPassword, 8 | @NonNull 9 | String newPassword){ 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/requests/UserAuthenticationRequests.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user.requests; 2 | 3 | import lombok.NonNull; 4 | 5 | public record UserAuthenticationRequests( 6 | @NonNull 7 | String emailAddress, 8 | @NonNull 9 | String password 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/response/AccountOverviewResponse.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account.response; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public record AccountOverviewResponse( 6 | BigDecimal accountBalance, 7 | String accountNumber, 8 | String TierLevel, 9 | String accountStatus 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/emailNotification/EmailSenderService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.emailNotification; 2 | 3 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.request.FundsAlertNotificationRequest; 4 | 5 | public interface EmailSenderService { 6 | 7 | void sendEmail(EmailNotification emailNotification);} 8 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/emailNotification/EmailNotification.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.emailNotification; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | @Data 7 | @AllArgsConstructor 8 | public class EmailNotification { 9 | private String receiverEmail; 10 | private String subject; 11 | private String message; 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | 2 | spring: 3 | datasource: 4 | url: jdbc:postgresql://localhost:5323/e_banking_suite_test 5 | username: amigoscode 6 | password: postgrespw 7 | jpa: 8 | hibernate: 9 | ddl-auto: create 10 | properties: 11 | hibernate: dialect:org.hibernate.dialect.PostgreSQLDialect 12 | format_sql: true 13 | show-sql: true 14 | main: 15 | web-application-type: servlet 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/requests/UserRegistrationRequest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user.requests; 2 | 3 | import lombok.NonNull; 4 | 5 | public record UserRegistrationRequest( 6 | @NonNull 7 | String fullName, 8 | @NonNull 9 | String emailAddress, 10 | @NonNull 11 | String password, 12 | @NonNull 13 | String phoneNumber 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/universal/ApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.universal; 2 | import lombok.AllArgsConstructor; 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @Setter 9 | @ToString 10 | @AllArgsConstructor 11 | public class ApiResponse { 12 | 13 | private String message; 14 | private Object data; 15 | 16 | 17 | public ApiResponse(String message) { 18 | this.message = message; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/CreditDebitSmsAlertTemplate.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public record CreditDebitSmsAlertTemplate( 6 | String senderName, 7 | String receiverName, 8 | String senderPhoneNumber, 9 | String receiverPhoneNumber, 10 | BigDecimal senderAccountBalance, 11 | BigDecimal receiverAccountBalance, 12 | BigDecimal amountTransferred 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/CreditDebitEmailAlertTemplate.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public record CreditDebitEmailAlertTemplate( 6 | String senderName, 7 | String receiverName, 8 | String senderEmailAddress, 9 | String receiverEmailAddress, 10 | BigDecimal senderAccountBalance, 11 | BigDecimal receiverAccountBalance, 12 | BigDecimal amountTransferred 13 | ) { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/emailNotification/request/FundsAlertNotificationRequest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.emailNotification.request; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | 6 | public record FundsAlertNotificationRequest( 7 | int senderId, 8 | int receiverId, 9 | BigDecimal senderNewAccountBalance, 10 | BigDecimal receiverNewAccountBalance, 11 | BigDecimal amountTransferred 12 | 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/request/FundsTransferRequest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction.request; 2 | 3 | import lombok.NonNull; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public record FundsTransferRequest( 8 | @NonNull 9 | String receiverAccountNumber, 10 | @NonNull 11 | String senderAccountNumber, 12 | @NonNull 13 | BigDecimal amount, 14 | @NonNull 15 | String transactionPin, 16 | String narration 17 | ) { 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/EBankingSuiteApplication.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | 7 | @SpringBootApplication 8 | @EnableAsync 9 | public class EBankingSuiteApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(EBankingSuiteApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import jakarta.persistence.criteria.CriteriaBuilder; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | import java.util.Optional; 7 | 8 | @Repository 9 | public interface UserRepository extends JpaRepository { 10 | 11 | boolean existsByEmailAddress(String emailAddress); 12 | Optional findByEmailAddress(String email); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/smsNotification/twilio/TwilioConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.smsNotification.twilio; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConfigurationProperties("twilio") 10 | @Getter@Setter 11 | public class TwilioConfiguration { 12 | private String accountSid; 13 | private String authToken; 14 | private String trialNumber; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/request/TransactionHistoryRequest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.NonNull; 5 | 6 | import java.time.LocalDateTime; 7 | 8 | public record TransactionHistoryRequest( 9 | @NonNull 10 | @JsonFormat(shape=JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy HH:mm:ss") 11 | LocalDateTime startDateTime, 12 | 13 | @NonNull 14 | @JsonFormat(shape=JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy HH:mm:ss") 15 | LocalDateTime endDateTime 16 | ) { 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * This is the interface for the account repository. 10 | */ 11 | @Repository 12 | public interface AccountRepository extends JpaRepository { 13 | Optional findAccountByUserId(Integer userId); 14 | Optional findAccountByAccountNumber(String accountNumber); 15 | boolean existsByAccountNumber(String accountNumber); 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/response/TransactionHistoryResponse.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction.response; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.math.BigDecimal; 9 | import java.time.LocalDateTime; 10 | 11 | @NoArgsConstructor 12 | @Getter 13 | @Setter 14 | @AllArgsConstructor 15 | public class TransactionHistoryResponse { 16 | LocalDateTime transactionDateTime; 17 | BigDecimal amount; 18 | TransactionType transactionType; 19 | String SenderName; 20 | String receiverName; 21 | String description; 22 | } 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | container_name: postgres_e_banking 4 | image: postgres 5 | environment: 6 | POSTGRES_USER: amigoscode 7 | POSTGRES_PASSWORD: postgrespw 8 | PGDATA: /data/postgres 9 | POSTGRES_DB: e_banking_suite 10 | volumes: 11 | - db:/var/lib/postgresql 12 | ports: 13 | - "5332:5432" 14 | networks: 15 | - db 16 | restart: unless-stopped 17 | 18 | test_db: 19 | container_name: postgres_e_banking_test 20 | image: postgres 21 | environment: 22 | POSTGRES_USER: amigoscode 23 | POSTGRES_PASSWORD: postgrespw 24 | PGDATA: /data/postgres 25 | POSTGRES_DB: e_banking_suite_test 26 | volumes: 27 | - db:/var/lib/postgresql 28 | ports: 29 | - "5323:5432" 30 | networks: 31 | - db 32 | restart: unless-stopped 33 | networks: 34 | db: 35 | driver: bridge 36 | volumes: 37 | db: 38 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/smsNotification/twilio/TwilioInitializer.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.smsNotification.twilio; 2 | 3 | import com.twilio.Twilio; 4 | import lombok.AllArgsConstructor; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class TwilioInitializer { 11 | 12 | private final static Logger logger = LoggerFactory.getLogger(TwilioInitializer.class); 13 | private final TwilioConfiguration twilioConfiguration; 14 | 15 | public TwilioInitializer(TwilioConfiguration twilioConfiguration) { 16 | this.twilioConfiguration = twilioConfiguration; 17 | Twilio.init(twilioConfiguration.getAccountSid(), 18 | twilioConfiguration.getAuthToken()); 19 | logger.info("Twilio initialization done.."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | error: 4 | include-message: always 5 | 6 | spring: 7 | datasource: 8 | url: jdbc:postgresql://localhost:5332/e_banking_suite 9 | username: amigoscode 10 | password: postgrespw 11 | jpa: 12 | hibernate: 13 | ddl-auto: update 14 | properties: 15 | hibernate: dialect:org.hibernate.dialect.PostgresSQLDialect 16 | format_sql: true 17 | show-sql: false 18 | main: 19 | web-application-type: servlet 20 | mail: 21 | host: smtp.gmail.com 22 | username: ebankingsuite@gmail.com 23 | password: fzlscatiopttfzek 24 | properties: 25 | "mail.smtp.socketFactory.port": 465 26 | "mail.smtp.auth": true 27 | "mail.smtp.socketFactory.class" : javax.net.ssl.SSLSocketFactory 28 | 29 | twilio: 30 | account_sid: AC8bd7bfd35c34219a8bdac215f9b31411 31 | auth_token: 90d71a0a3d9bd1d09a4c292a181293b4 32 | trial_number: +15077107547 -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | import org.springframework.data.domain.Page; 3 | import org.springframework.data.domain.Pageable; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface TransactionRepository extends JpaRepository { 11 | boolean existsByReferenceNum(String referenceNumber); 12 | 13 | Page findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 14 | TransactionStatus status,LocalDateTime startDate,LocalDateTime endDate, String senderAccountNumber, String receiverAccountNumber, Pageable pageable 15 | ); 16 | 17 | List findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 18 | TransactionStatus status,LocalDateTime startDate,LocalDateTime endDate, String senderAccountNumber, String receiverAccountNumber); 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/smsNotification/twilio/TwilioSmsSenderService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.smsNotification.twilio; 2 | 3 | import com.amigoscode.group.ebankingsuite.notification.smsNotification.SmsNotification; 4 | import com.amigoscode.group.ebankingsuite.notification.smsNotification.SmsSenderService; 5 | import com.twilio.rest.api.v2010.account.Message; 6 | import com.twilio.type.PhoneNumber; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | public class TwilioSmsSenderService implements SmsSenderService { 11 | 12 | private final TwilioConfiguration twilioConfiguration; 13 | 14 | public TwilioSmsSenderService(TwilioConfiguration twilioConfiguration) { 15 | this.twilioConfiguration = twilioConfiguration; 16 | } 17 | 18 | @Override 19 | public void sendSms(SmsNotification smsNotification) { 20 | Message.creator( 21 | new PhoneNumber(smsNotification.receiverPhoneNumber()), 22 | new PhoneNumber(twilioConfiguration.getTrialNumber()), 23 | smsNotification.message() 24 | ).create(); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/transaction/TransactionRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import static org.assertj.core.api.Java6Assertions.assertThat; 4 | import org.junit.jupiter.api.AfterEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 8 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 9 | 10 | import java.math.BigDecimal; 11 | 12 | @DataJpaTest 13 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 14 | public class TransactionRepositoryTest { 15 | 16 | @Autowired 17 | private TransactionRepository transactionRepository; 18 | 19 | @AfterEach 20 | void deleteDataInTable(){ 21 | transactionRepository.deleteAll(); 22 | } 23 | 24 | @Test 25 | void canCheckIfReferenceNumberExists(){ 26 | //given 27 | Transaction newTransaction = new Transaction( 28 | "6765456456", 29 | "6765456434", 30 | new BigDecimal(50), 31 | "dvhbjh37878", 32 | "test transaction", 33 | TransactionStatus.SUCCESS, 34 | "test1", 35 | "test2" 36 | ); 37 | transactionRepository.save(newTransaction); 38 | //when 39 | boolean status = transactionRepository.existsByReferenceNum(newTransaction.getReferenceNum()); 40 | //then 41 | assertThat(status).isTrue(); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/config/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.authentication.AuthenticationProvider; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 9 | import org.springframework.security.config.http.SessionCreationPolicy; 10 | import org.springframework.security.web.SecurityFilterChain; 11 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | @RequiredArgsConstructor 16 | public class SecurityConfiguration { 17 | 18 | private final JWTFilter jwtAuthFilter; 19 | private final AuthenticationProvider authenticationProvider; 20 | 21 | 22 | 23 | @Bean 24 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 25 | http 26 | .csrf() 27 | .disable() 28 | .authorizeHttpRequests() 29 | .requestMatchers("/api/v1/users/**") 30 | .permitAll() 31 | .anyRequest() 32 | .authenticated() 33 | .and() 34 | .sessionManagement() 35 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 36 | .and() 37 | .authenticationProvider(authenticationProvider) 38 | .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); 39 | return http.build(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/emailNotification/gmail/GmailEmailSenderService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification.emailNotification.gmail; 2 | 3 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.EmailNotification; 4 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.EmailSenderService; 5 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.request.FundsAlertNotificationRequest; 6 | import com.amigoscode.group.ebankingsuite.user.User; 7 | import com.amigoscode.group.ebankingsuite.user.UserService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.mail.MailException; 10 | import org.springframework.mail.SimpleMailMessage; 11 | import org.springframework.mail.javamail.JavaMailSender; 12 | import org.springframework.scheduling.annotation.Async; 13 | import org.springframework.stereotype.Service; 14 | 15 | @Service 16 | public class GmailEmailSenderService implements EmailSenderService { 17 | 18 | @Autowired 19 | private JavaMailSender mailSender; 20 | 21 | @Override 22 | public void sendEmail(EmailNotification emailNotification) { 23 | try{ 24 | // Creating a simple mail message object 25 | SimpleMailMessage emailMessage = new SimpleMailMessage(); 26 | String sender = "ebankingsuite@gmail.com"; 27 | emailMessage.setFrom(sender); 28 | emailMessage.setTo(emailNotification.getReceiverEmail()); 29 | emailMessage.setSubject(emailNotification.getSubject()); 30 | emailMessage.setText(emailNotification.getMessage()); 31 | mailSender.send(emailMessage); 32 | }catch (MailException e){ 33 | System.out.println(e.getMessage()); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/user/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 7 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 8 | 9 | import java.util.Optional; 10 | 11 | import static org.assertj.core.api.Java6Assertions.assertThat; 12 | 13 | @DataJpaTest 14 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 15 | class UserRepositoryTest{ 16 | 17 | @Autowired 18 | private UserRepository userRepository; 19 | @AfterEach 20 | void deleteDataInTable(){ 21 | userRepository.deleteAll(); 22 | } 23 | 24 | @Test 25 | void canCheckIfUserExistsByEmailAddress(){ 26 | //given 27 | String email = "ebanking@gmail.com"; 28 | User testUser = new User( 29 | "test pater", 30 | "ebanking@gmail.com", 31 | "1234", 32 | true, 33 | "+23478768990" 34 | ); 35 | userRepository.save(testUser); 36 | 37 | //when 38 | boolean expected = userRepository.existsByEmailAddress(email); 39 | 40 | //then 41 | assertThat(expected).isTrue(); 42 | } 43 | 44 | @Test 45 | void canReturnOptionalOfUserByEmailAddress(){ 46 | //given 47 | String emailAddress ="ebanking@gmail.com"; 48 | User testUser = new User( 49 | "test pater", 50 | "ebanking@gmail.com", 51 | "1234", 52 | true, 53 | "+2348709090" 54 | ); 55 | 56 | userRepository.save(testUser); 57 | 58 | //when 59 | Optional expectedUser = userRepository.findByEmailAddress(emailAddress); 60 | 61 | //then 62 | assertThat(expectedUser).isEqualTo(Optional.of(testUser)); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/transaction/TransactionControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import com.amigoscode.group.ebankingsuite.config.JWTService; 4 | import com.amigoscode.group.ebankingsuite.transaction.request.FundsTransferRequest; 5 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.mock.web.MockHttpServletRequest; 14 | import org.springframework.web.context.request.RequestContextHolder; 15 | import org.springframework.web.context.request.ServletRequestAttributes; 16 | 17 | import java.math.BigDecimal; 18 | 19 | import static org.assertj.core.api.Java6Assertions.assertThat; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | class TransactionControllerTest { 23 | 24 | private TransactionController transactionController; 25 | @Mock 26 | private TransactionService transactionService; 27 | @Mock 28 | private JWTService jwtService; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | this.transactionController = new TransactionController(transactionService, jwtService); 33 | } 34 | 35 | @Test 36 | void transferFunds_successful() { 37 | //given 38 | FundsTransferRequest fundsTransferRequest = new FundsTransferRequest("165568799", "986562737", new BigDecimal(200), "1224", "test transfer"); 39 | 40 | MockHttpServletRequest request = new MockHttpServletRequest(); 41 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 42 | 43 | //when 44 | ResponseEntity responseEntity = transactionController.transferFunds(fundsTransferRequest); 45 | //then 46 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.config; 2 | 3 | import com.amigoscode.group.ebankingsuite.user.UserRepository; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.AuthenticationProvider; 9 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 10 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 14 | import org.springframework.security.crypto.password.PasswordEncoder; 15 | 16 | @Configuration 17 | @RequiredArgsConstructor 18 | public class ApplicationConfig { 19 | 20 | private final UserRepository userRepository; 21 | 22 | @Bean 23 | public UserDetailsService userDetailsService() { 24 | return username -> userRepository.findByEmailAddress(username) 25 | .orElseThrow(() -> new UsernameNotFoundException("User not found")); 26 | } 27 | 28 | @Bean 29 | public AuthenticationProvider authenticationProvider() { 30 | DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); 31 | authProvider.setUserDetailsService(userDetailsService()); 32 | authProvider.setPasswordEncoder(passwordEncoder()); 33 | return authProvider; 34 | } 35 | 36 | @Bean 37 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { 38 | return config.getAuthenticationManager(); 39 | } 40 | 41 | @Bean 42 | public PasswordEncoder passwordEncoder() { 43 | return new BCryptPasswordEncoder(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/Transaction.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.NonNull; 6 | import java.math.BigDecimal; 7 | import java.time.LocalDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | @Entity 11 | @Table(name = "transactions") 12 | @Data 13 | public class Transaction { 14 | 15 | @Id 16 | @SequenceGenerator( 17 | name = "transaction_id_sequence", 18 | sequenceName = "transaction_id_sequence" 19 | ) 20 | @GeneratedValue( 21 | strategy = GenerationType.SEQUENCE, 22 | generator = "transaction_id_sequence" 23 | ) 24 | private Integer id; 25 | @NonNull 26 | private String senderAccountNumber; 27 | @NonNull 28 | private String receiverAccountNumber; 29 | 30 | @NonNull 31 | private String senderName; 32 | @NonNull 33 | private String receiverName; 34 | @NonNull 35 | private BigDecimal amount; 36 | @NonNull 37 | private String referenceNum; 38 | private String description; 39 | @Enumerated(EnumType.STRING) 40 | @NonNull 41 | private TransactionStatus status; 42 | private LocalDateTime createdAt; 43 | private LocalDateTime updatedAt; 44 | 45 | private static final DateTimeFormatter DATE_TIME_FORMATTER = 46 | DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); 47 | 48 | public Transaction(String senderAccountNumber, String receiverAccountNumber, BigDecimal amount, String referenceNum, String description, TransactionStatus status, String senderName, String receiverName) { 49 | this.senderAccountNumber = senderAccountNumber; 50 | this.receiverAccountNumber = receiverAccountNumber; 51 | this.amount = amount; 52 | this.referenceNum = referenceNum; 53 | this.description = description; 54 | this.status = status; 55 | this.createdAt = LocalDateTime.parse( 56 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 57 | DATE_TIME_FORMATTER); 58 | this.updatedAt = createdAt; 59 | this.senderName = senderName; 60 | this.receiverName = receiverName; 61 | } 62 | 63 | public Transaction(){ 64 | this.createdAt = LocalDateTime.parse( 65 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 66 | DATE_TIME_FORMATTER); 67 | this.updatedAt = createdAt; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/Account.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | 4 | import jakarta.persistence.*; 5 | import lombok.*; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | 12 | @Entity 13 | @Table(name = "accounts") 14 | @Data 15 | @AllArgsConstructor 16 | public class Account { 17 | 18 | @Id 19 | @SequenceGenerator( 20 | name = "account_id_sequence", 21 | sequenceName = "account_id_sequence" 22 | ) 23 | @GeneratedValue( 24 | strategy = GenerationType.SEQUENCE, 25 | generator = "account_id_sequence" 26 | ) 27 | private Integer Id; 28 | private Integer userId; 29 | private BigDecimal accountBalance; 30 | @Enumerated(EnumType.STRING) 31 | private AccountStatus accountStatus; 32 | private String accountNumber; 33 | @Enumerated(EnumType.STRING) 34 | private Tier tierLevel; 35 | private String transactionPin; 36 | private LocalDateTime createdAt; 37 | private LocalDateTime updatedAt; 38 | 39 | private static final DateTimeFormatter DATE_TIME_FORMATTER = 40 | DateTimeFormatter.ofPattern("d/M/yyyy HH:mm:ss"); 41 | 42 | public Account(Integer userId){ 43 | this.setUserId(userId); 44 | this.createdAt = LocalDateTime.parse( 45 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 46 | DATE_TIME_FORMATTER); 47 | this.updatedAt = createdAt; 48 | this.setAccountBalance(new BigDecimal(0)); 49 | } 50 | public Account(){ 51 | this.createdAt = LocalDateTime.parse( 52 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 53 | DATE_TIME_FORMATTER); 54 | this.updatedAt = createdAt; 55 | this.setAccountBalance(new BigDecimal(0)); 56 | } 57 | 58 | public Account(Integer userId, BigDecimal accountBalance, AccountStatus accountStatus, String accountNumber, Tier tierLevel, String transactionPin) { 59 | this.userId = userId; 60 | this.accountBalance = accountBalance; 61 | this.accountStatus = accountStatus; 62 | this.accountNumber = accountNumber; 63 | this.tierLevel = tierLevel; 64 | this.transactionPin = transactionPin; 65 | this.createdAt = LocalDateTime.parse( 66 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 67 | DATE_TIME_FORMATTER); 68 | this.updatedAt = createdAt; 69 | } 70 | } -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/account/AccountRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import org.junit.jupiter.api.AfterEach; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 7 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.Optional; 11 | 12 | import static org.assertj.core.api.Java6Assertions.assertThat; 13 | 14 | @DataJpaTest 15 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 16 | class AccountRepositoryTest { 17 | 18 | @Autowired 19 | private AccountRepository accountRepository; 20 | 21 | @AfterEach 22 | void deleteDataInTable(){ 23 | accountRepository.deleteAll(); 24 | } 25 | 26 | @Test 27 | void canFindAccountByUserId() { 28 | //given 29 | Integer userId = 1; 30 | Account testAccount = new Account( 31 | 1, 32 | new BigDecimal(0), 33 | AccountStatus.ACTIVATED, 34 | "12345456", 35 | Tier.LEVEL1, 36 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 37 | accountRepository.save(testAccount); 38 | 39 | //when 40 | Optional account = accountRepository.findAccountByUserId(userId); 41 | 42 | //then 43 | assertThat(account.get()).isEqualTo(testAccount); 44 | } 45 | 46 | @Test 47 | void canFindAccountByAccountNumber() { 48 | //given 49 | String accountNumber = "675267538"; 50 | Account testAccount = new Account( 51 | 1, 52 | new BigDecimal(0), 53 | AccountStatus.ACTIVATED, 54 | "675267538", 55 | Tier.LEVEL1, 56 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 57 | accountRepository.save(testAccount); 58 | 59 | //when 60 | Optional account = accountRepository.findAccountByAccountNumber(accountNumber); 61 | 62 | //then 63 | assertThat(account.get()).isEqualTo(testAccount); 64 | } 65 | 66 | @Test 67 | void canCheckIfAccountExistsByAccountNumber() { 68 | //given 69 | String accountNumber = "675267538"; 70 | //when 71 | boolean status = accountRepository.existsByAccountNumber(accountNumber); 72 | 73 | //then 74 | assertThat(status).isFalse(); 75 | } 76 | } -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/UserController.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import com.amigoscode.group.ebankingsuite.config.JWTService; 4 | import com.amigoscode.group.ebankingsuite.exception.ValueMismatchException; 5 | import com.amigoscode.group.ebankingsuite.user.requests.ChangePasswordRequest; 6 | import com.amigoscode.group.ebankingsuite.user.requests.UserAuthenticationRequests; 7 | import com.amigoscode.group.ebankingsuite.user.requests.UserRegistrationRequest; 8 | import com.amigoscode.group.ebankingsuite.exception.InvalidAuthenticationException; 9 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 10 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.ResponseEntity; 14 | import org.springframework.validation.annotation.Validated; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | @RestController 18 | @RequestMapping("/api/v1/users") 19 | public class UserController { 20 | 21 | private final UserService userService; 22 | private final JWTService jwtService; 23 | 24 | @Autowired 25 | public UserController(UserService userService, JWTService jwtService) { 26 | this.userService = userService; 27 | this.jwtService = jwtService; 28 | } 29 | 30 | @PostMapping() 31 | public ResponseEntity saveUser(@RequestBody @Validated UserRegistrationRequest request){ 32 | userService.createNewUser(request); 33 | return new ResponseEntity<>( 34 | new ApiResponse("user created successfully"), HttpStatus.CREATED); 35 | } 36 | 37 | @PostMapping("/login") 38 | public ResponseEntity authenticateUser( 39 | @RequestBody @Validated UserAuthenticationRequests userAuthenticationRequests){ 40 | 41 | String jwt = userService.authenticateUser(userAuthenticationRequests); 42 | return new ResponseEntity<>( 43 | new ApiResponse("user logged in successfully",jwt),HttpStatus.OK); 44 | } 45 | 46 | @PutMapping("/change-password") 47 | public ResponseEntity changeUserPassword(@RequestHeader("Authorization") String jwt, 48 | @RequestBody @Validated ChangePasswordRequest request){ 49 | userService.changeUserPassword(request,jwtService.extractUserIdFromToken(jwt)); 50 | return new ResponseEntity<>(new ApiResponse("password changed"),HttpStatus.OK); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/config/JWTFilter.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.config; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.context.SecurityContextHolder; 10 | import org.springframework.security.core.userdetails.UserDetails; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.web.filter.OncePerRequestFilter; 15 | 16 | import java.io.IOException; 17 | 18 | @Component 19 | @RequiredArgsConstructor 20 | public class JWTFilter extends OncePerRequestFilter { 21 | 22 | private final JWTService jwtService; 23 | private final UserDetailsService userDetailsService; 24 | 25 | 26 | 27 | @Override 28 | protected void doFilterInternal(HttpServletRequest request, 29 | HttpServletResponse response, 30 | FilterChain filterChain) 31 | throws ServletException, IOException { 32 | 33 | final String authHeader = request.getHeader("Authorization"); 34 | final String jwt; 35 | final String userEmail; 36 | 37 | if (authHeader == null || !authHeader.startsWith("Bearer ")) { 38 | filterChain.doFilter(request, response); 39 | return; 40 | } 41 | jwt = authHeader.substring(7); 42 | userEmail = jwtService.extractUsername(jwt); 43 | if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { 44 | UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); 45 | if (jwtService.isTokenValid(jwt, userDetails)) { 46 | UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( 47 | userDetails, 48 | null, 49 | userDetails.getAuthorities() 50 | ); 51 | 52 | authToken.setDetails( 53 | new WebAuthenticationDetailsSource().buildDetails(request) 54 | ); 55 | SecurityContextHolder.getContext().setAuthentication(authToken); 56 | } 57 | } 58 | filterChain.doFilter(request, response); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/exception/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.exception; 2 | 3 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 4 | import com.itextpdf.text.DocumentException; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.ControllerAdvice; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 10 | 11 | @ControllerAdvice 12 | public class CustomExceptionHandler extends ResponseEntityExceptionHandler { 13 | 14 | @ExceptionHandler(ResourceExistsException.class) 15 | public final ResponseEntity handleResourceExistsException(ResourceExistsException exception){ 16 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.CONFLICT); 17 | } 18 | @ExceptionHandler(ResourceNotFoundException.class) 19 | public final ResponseEntity handleResourceNotFoundException(ResourceNotFoundException exception){ 20 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.NOT_FOUND); 21 | } 22 | 23 | @ExceptionHandler(AccountNotActivatedException.class) 24 | public final ResponseEntity handleAccountNotActivatedException(AccountNotActivatedException exception){ 25 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.FORBIDDEN); 26 | } 27 | @ExceptionHandler(AccountNotClearedException.class) 28 | public final ResponseEntity handleAccountNotClearedException(AccountNotClearedException exception){ 29 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.NOT_ACCEPTABLE); 30 | } 31 | @ExceptionHandler(InsufficientBalanceException.class) 32 | public final ResponseEntity handleInsufficientBalanceException(InsufficientBalanceException exception){ 33 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.BAD_REQUEST); 34 | } 35 | @ExceptionHandler(InvalidAuthenticationException.class) 36 | public final ResponseEntity handleInvalidAuthenticationException(InvalidAuthenticationException exception){ 37 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.UNAUTHORIZED); 38 | } 39 | @ExceptionHandler(InvalidAuthorizationException.class) 40 | public final ResponseEntity handleInvalidAuthorizationException(InvalidAuthorizationException exception){ 41 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.UNAUTHORIZED); 42 | } 43 | @ExceptionHandler(ValueMismatchException.class) 44 | public final ResponseEntity handleValueMismatchException(ValueMismatchException exception){ 45 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.BAD_REQUEST); 46 | } 47 | @ExceptionHandler(DocumentException.class) 48 | public final ResponseEntity handleDocumentException(ValueMismatchException exception){ 49 | return new ResponseEntity<>(new ApiResponse(exception.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/config/JWTService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.config; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | import io.jsonwebtoken.io.Decoders; 7 | import io.jsonwebtoken.security.Keys; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.security.Key; 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.function.Function; 16 | 17 | @Service 18 | public class JWTService { 19 | 20 | private static final String SECRET_KEY = "DEC897856524D80723AC9FE48DDEDA830F8F40C8FC77BDBA1E81A2B88F5A41F0"; 21 | public String extractUsername(String token) { 22 | 23 | return extractClaim( 24 | token, Claims::getSubject); 25 | } 26 | 27 | public T extractClaim(String token, Function claimsResolver) { 28 | final Claims claims = extractAllClaims(token); 29 | return claimsResolver.apply(claims); 30 | } 31 | 32 | public Integer extractUserIdFromToken(String token){ 33 | return extractAllClaims(trimTokenOfBearerText(token)).get("userId", Integer.class); 34 | } 35 | 36 | private String trimTokenOfBearerText(String token){ 37 | return token.substring(7); 38 | } 39 | 40 | 41 | 42 | public String generateToken(UserDetails userDetails) { 43 | return generateToken(new HashMap<>(), userDetails); 44 | } 45 | 46 | public String generateToken( 47 | Map extraClaims, 48 | UserDetails userDetails 49 | ) { 50 | return Jwts 51 | .builder() 52 | .setClaims(extraClaims) 53 | .setSubject(userDetails.getUsername()) 54 | .setIssuedAt(new Date(System.currentTimeMillis())) 55 | .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 24)) 56 | .signWith(getSignInKey(), SignatureAlgorithm.HS256) 57 | .compact(); 58 | } 59 | 60 | public boolean isTokenValid(String token, UserDetails userDetails) { 61 | final String username = extractUsername(token); 62 | return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); 63 | } 64 | 65 | private boolean isTokenExpired(String token) { 66 | return extractExpiration(token).before(new Date()); 67 | } 68 | 69 | private Date extractExpiration(String token) { 70 | return extractClaim(token, Claims::getExpiration); 71 | } 72 | 73 | private Claims extractAllClaims(String token) { 74 | return Jwts 75 | .parserBuilder() 76 | .setSigningKey(getSignInKey()) 77 | .build() 78 | .parseClaimsJws(token) 79 | .getBody(); 80 | 81 | } 82 | 83 | private Key getSignInKey() { 84 | byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); 85 | return Keys.hmacShaKeyFor(keyBytes); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/TransactionController.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import com.amigoscode.group.ebankingsuite.config.JWTService; 4 | import com.amigoscode.group.ebankingsuite.transaction.request.FundsTransferRequest; 5 | import com.amigoscode.group.ebankingsuite.transaction.request.TransactionHistoryRequest; 6 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 7 | import com.itextpdf.text.DocumentException; 8 | import org.springframework.data.domain.PageRequest; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.http.*; 11 | import org.springframework.validation.annotation.Validated; 12 | import org.springframework.web.bind.annotation.*; 13 | import java.io.ByteArrayOutputStream; 14 | 15 | @RestController 16 | @RequestMapping("/api/v1/transactions") 17 | public class TransactionController { 18 | 19 | private final TransactionService transactionService; 20 | private final JWTService jwtService; 21 | 22 | public TransactionController(TransactionService transactionService, JWTService jwtService) { 23 | this.transactionService = transactionService; 24 | this.jwtService = jwtService; 25 | } 26 | 27 | @PostMapping("/send-funds") 28 | public ResponseEntity transferFunds(@RequestBody @Validated FundsTransferRequest request){ 29 | 30 | transactionService.transferFunds(request); 31 | return new ResponseEntity<>(new ApiResponse("funds transferred"), HttpStatus.OK); 32 | } 33 | 34 | /** 35 | * This controller fetches all transaction based on a range of date and also implements pagination to help reduce load time 36 | */ 37 | @PostMapping() 38 | public ResponseEntity generateTransactionHistory(@RequestHeader("Authorization") String jwt, 39 | @RequestParam int size, 40 | @RequestParam int page, 41 | @RequestBody @Validated TransactionHistoryRequest request) { 42 | Pageable pageable = PageRequest.of(page, size); 43 | return new ResponseEntity<>(new ApiResponse("transaction history", 44 | transactionService.getTransactionHistoryByUserId(request,jwtService.extractUserIdFromToken(jwt),pageable)), 45 | HttpStatus.OK); 46 | } 47 | 48 | /** 49 | * This controller generates monthly or yearly account statement in pdf format 50 | */ 51 | @PostMapping(value = "/transaction-report", produces = MediaType.APPLICATION_PDF_VALUE) 52 | public ResponseEntity generateTransactionStatement( 53 | @RequestHeader("Authorization") String jwt, 54 | @RequestBody TransactionHistoryRequest request) throws DocumentException { 55 | 56 | int userId = jwtService.extractUserIdFromToken(jwt); 57 | HttpHeaders headers = new HttpHeaders(); 58 | headers.setContentType(MediaType.APPLICATION_PDF); 59 | headers.setContentDisposition( 60 | ContentDisposition.builder("inline") 61 | .filename("transaction_report.pdf") 62 | .build() 63 | ); 64 | 65 | ByteArrayOutputStream outputStream = transactionService.generateTransactionStatement( 66 | request,userId 67 | ); 68 | 69 | 70 | return new ResponseEntity<>(outputStream.toByteArray(), headers, HttpStatus.OK); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/notification/NotificationSenderService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.notification; 2 | 3 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.EmailNotification; 4 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.EmailSenderService; 5 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.request.FundsAlertNotificationRequest; 6 | import com.amigoscode.group.ebankingsuite.notification.smsNotification.SmsNotification; 7 | import com.amigoscode.group.ebankingsuite.notification.smsNotification.twilio.TwilioSmsSenderService; 8 | import com.amigoscode.group.ebankingsuite.user.User; 9 | import com.amigoscode.group.ebankingsuite.user.UserService; 10 | import org.springframework.scheduling.annotation.Async; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class NotificationSenderService { 15 | 16 | private final UserService userService; 17 | private final EmailSenderService emailSenderService; 18 | private final TwilioSmsSenderService smsSenderService; 19 | 20 | public NotificationSenderService(UserService userService, EmailSenderService emailSenderService, TwilioSmsSenderService smsSenderService) { 21 | this.userService = userService; 22 | this.emailSenderService = emailSenderService; 23 | 24 | this.smsSenderService = smsSenderService; 25 | } 26 | 27 | 28 | @Async 29 | public void sendCreditAndDebitNotification(FundsAlertNotificationRequest request){ 30 | User senderUser = userService.getUserByUserId(request.senderId()); 31 | User receiverUser = userService.getUserByUserId(request.receiverId()); 32 | 33 | sendCreditDebitEmailAlertToCustomer(new CreditDebitEmailAlertTemplate( 34 | senderUser.getFullName(), 35 | receiverUser.getFullName(), 36 | senderUser.getEmailAddress(), 37 | receiverUser.getEmailAddress(), 38 | request.senderNewAccountBalance(), 39 | request.receiverNewAccountBalance(), 40 | request.amountTransferred() 41 | )); 42 | 43 | sendCreditDebitSmsAlertToCustomer(new CreditDebitSmsAlertTemplate( 44 | senderUser.getFullName(), 45 | receiverUser.getFullName(), 46 | senderUser.getPhoneNumber(), 47 | receiverUser.getPhoneNumber(), 48 | request.senderNewAccountBalance(), 49 | request.receiverNewAccountBalance(), 50 | request.amountTransferred() 51 | )); 52 | 53 | 54 | } 55 | 56 | public void sendCreditDebitEmailAlertToCustomer(CreditDebitEmailAlertTemplate template) { 57 | 58 | //credit alert for receiver 59 | final String receiverMessage = "Money In! You have been credited USD" + template.amountTransferred() + 60 | " from " + template.senderName() + ". You have USD" + template.receiverAccountBalance(); 61 | 62 | emailSenderService.sendEmail(new EmailNotification( 63 | template.receiverEmailAddress(), 64 | "CREDIT ALERT", 65 | receiverMessage 66 | )); 67 | 68 | //debit alert for sender 69 | final String senderMessage = "Money Out! You have sent USD" + template.amountTransferred() + 70 | " to " + template.receiverName() + ". You have USD" + template.senderAccountBalance(); 71 | emailSenderService.sendEmail(new EmailNotification( 72 | template.senderEmailAddress(), 73 | "DEBIT ALERT", 74 | senderMessage)); 75 | } 76 | 77 | public void sendCreditDebitSmsAlertToCustomer(CreditDebitSmsAlertTemplate template){ 78 | 79 | //credit alert for receiver 80 | final String receiverMessage = "Money Out! You have sent USD" + template.amountTransferred() + 81 | " to " + template.receiverName() + ". You have USD" + template.senderAccountBalance(); 82 | 83 | smsSenderService.sendSms(new SmsNotification( 84 | template.receiverPhoneNumber(), 85 | receiverMessage 86 | )); 87 | 88 | //debit alert for sender 89 | final String senderMessage = "Money In! You have been credited USD" + template.amountTransferred() + 90 | " from " + template.senderName() + ". You have USD" + template.receiverAccountBalance(); 91 | 92 | smsSenderService.sendSms(new SmsNotification( 93 | template.receiverPhoneNumber(), 94 | senderMessage 95 | )); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/User.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import java.time.LocalDateTime; 9 | import java.time.format.DateTimeFormatter; 10 | import java.util.Collection; 11 | import java.util.Objects; 12 | 13 | @Entity 14 | @Table(name = "users") 15 | @Data 16 | @NoArgsConstructor 17 | public class User implements UserDetails { 18 | 19 | @Id 20 | @SequenceGenerator( 21 | name = "user_id_sequence", 22 | sequenceName = "user_id_sequence" 23 | ) 24 | @GeneratedValue( 25 | strategy = GenerationType.SEQUENCE, 26 | generator = "user_id_sequence" 27 | ) 28 | private Integer id; 29 | @Column(nullable = false) 30 | private String fullName; 31 | @Column(nullable = false) 32 | private String emailAddress; 33 | @Column(nullable = false) 34 | private String password; 35 | @Column(nullable = false) 36 | private String phoneNumber; 37 | @Column(nullable = false) 38 | private boolean isNotBlocked; 39 | private LocalDateTime createdAt; 40 | private LocalDateTime updatedAt; 41 | 42 | private static final DateTimeFormatter DATE_TIME_FORMATTER = 43 | DateTimeFormatter.ofPattern("d/M/yyyy HH:mm:ss"); 44 | 45 | public User(String fullName, String emailAddress, String password, boolean isNotBlocked, String phoneNumber) { 46 | this.fullName = fullName; 47 | this.emailAddress = emailAddress; 48 | this.password = password; 49 | this.createdAt = LocalDateTime.parse( 50 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 51 | DATE_TIME_FORMATTER); 52 | this.updatedAt = createdAt; 53 | this.isNotBlocked = isNotBlocked; 54 | this.phoneNumber= phoneNumber; 55 | } 56 | 57 | /** 58 | * This constructor is for test purpose. Id must always be auto generated 59 | */ 60 | public User(Integer id,String fullName, String emailAddress, String password, boolean isNotBlocked) { 61 | this.id = id; 62 | this.fullName = fullName; 63 | this.emailAddress = emailAddress; 64 | this.password = password; 65 | this.createdAt = LocalDateTime.parse( 66 | DATE_TIME_FORMATTER.format(LocalDateTime.now()), 67 | DATE_TIME_FORMATTER); 68 | this.updatedAt = createdAt; 69 | this.isNotBlocked = isNotBlocked; 70 | } 71 | 72 | @Override 73 | public Collection getAuthorities() { 74 | return null; 75 | } 76 | 77 | public String getPassword() { 78 | return password; 79 | } 80 | 81 | @Override 82 | public String getUsername() { 83 | return emailAddress; 84 | } 85 | 86 | @Override 87 | public boolean isAccountNonExpired() { 88 | return true; 89 | } 90 | 91 | @Override 92 | public boolean isAccountNonLocked() { 93 | return isNotBlocked; 94 | } 95 | 96 | @Override 97 | public boolean isCredentialsNonExpired() { 98 | return true; 99 | } 100 | 101 | @Override 102 | public boolean isEnabled() { 103 | return true; 104 | } 105 | 106 | 107 | @Override 108 | public String toString() { 109 | return "User{" + 110 | "id=" + id + 111 | ", fullName='" + fullName + '\'' + 112 | ", emailAddress='" + emailAddress + '\'' + 113 | ", password='" + password + '\'' + 114 | ", isNotBlocked=" + isNotBlocked + 115 | ", createdAt=" + createdAt + 116 | ", updatedAt=" + updatedAt + 117 | '}'; 118 | } 119 | 120 | @Override 121 | public boolean equals(Object o) { 122 | if (this == o) return true; 123 | if (o == null || getClass() != o.getClass()) return false; 124 | User user = (User) o; 125 | return isNotBlocked == user.isNotBlocked && Objects.equals(id, user.id) && Objects.equals(fullName, user.fullName) && Objects.equals(emailAddress, user.emailAddress) && Objects.equals(password, user.password) && Objects.equals(createdAt, user.createdAt) && Objects.equals(updatedAt, user.updatedAt); 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | return Objects.hash(id, fullName, emailAddress, password, isNotBlocked, createdAt, updatedAt); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.0.2 10 | 11 | 12 | com.amogoscode.groupe 13 | e-banking-suite 14 | 0.0.1-SNAPSHOT 15 | e-banking-suite 16 | e-banking-suite 17 | 18 | 17 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-mail 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-security 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-devtools 41 | runtime 42 | true 43 | 44 | 45 | org.postgresql 46 | postgresql 47 | runtime 48 | 49 | 50 | org.projectlombok 51 | lombok 52 | true 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-test 57 | test 58 | 59 | 60 | com.itextpdf 61 | itextpdf 62 | 5.5.13.3 63 | 64 | 65 | org.springframework.security 66 | spring-security-test 67 | test 68 | 69 | 70 | org.assertj 71 | assertj-core 72 | 3.4.1 73 | test 74 | 75 | 76 | io.jsonwebtoken 77 | jjwt-api 78 | 0.11.5 79 | 80 | 81 | io.jsonwebtoken 82 | jjwt-impl 83 | 0.11.5 84 | runtime 85 | 86 | 87 | io.jsonwebtoken 88 | jjwt-jackson 89 | 0.11.5 90 | 91 | 92 | junit 93 | junit 94 | test 95 | 96 | 97 | com.twilio.sdk 98 | twilio 99 | 9.3.0 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.springframework.boot 107 | spring-boot-maven-plugin 108 | 109 | 110 | 111 | org.projectlombok 112 | lombok 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.request.AccountTransactionPinUpdateModel; 4 | import com.amigoscode.group.ebankingsuite.account.response.AccountOverviewResponse; 5 | import com.amigoscode.group.ebankingsuite.config.JWTService; 6 | import com.amigoscode.group.ebankingsuite.exception.AccountNotClearedException; 7 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 8 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.validation.annotation.Validated; 13 | import org.springframework.web.bind.annotation.*; 14 | 15 | @RestController 16 | @RequestMapping("/api/v1/accounts") 17 | public class AccountController { 18 | 19 | private final AccountService accountService; 20 | private final JWTService jwtService; 21 | 22 | @Autowired 23 | public AccountController(AccountService accountService, JWTService jwtService) { 24 | this.accountService = accountService; 25 | this.jwtService = jwtService; 26 | } 27 | 28 | /** 29 | * This controller fetches the user account overview by getting the userId from the JWT token 30 | */ 31 | @GetMapping("/overview") 32 | public ResponseEntity getUserAccountOverview( 33 | @RequestHeader("Authorization") String jwt) { 34 | try { 35 | AccountOverviewResponse response = accountService.generateAccountOverviewByUserId( 36 | jwtService.extractUserIdFromToken(jwt)); 37 | return new ResponseEntity<>(new ApiResponse("user account overview", response), 38 | HttpStatus.OK); 39 | } catch (ResourceNotFoundException e) { 40 | return new ResponseEntity<>(new ApiResponse(e.getMessage()), HttpStatus.NOT_FOUND); 41 | } 42 | 43 | } 44 | 45 | /** 46 | * This controller allows user to close their account by getting the userId from the JWT and the relieving reason 47 | * from the request body 48 | */ 49 | @DeleteMapping("/close") 50 | public ResponseEntity closeAccount( 51 | @RequestHeader("Authorization") String jwt) { 52 | accountService.closeAccount(jwtService.extractUserIdFromToken(jwt)); 53 | return new ResponseEntity<>(new ApiResponse("account closed successfully"), HttpStatus.OK); 54 | } 55 | @PutMapping("/transaction-pin") 56 | public ResponseEntity updateAccountTransactionPin( 57 | @RequestHeader("Authorization") String jwt, @RequestBody @Validated AccountTransactionPinUpdateModel pinUpdateModel) { 58 | 59 | accountService.updateAccountTransactionPin(jwtService.extractUserIdFromToken(jwt),pinUpdateModel); 60 | return new ResponseEntity<>(new ApiResponse("transaction pin set"), HttpStatus.OK); 61 | } 62 | 63 | /* @PostMapping("/profile") 64 | public ResponseEntity createAccount(@RequestBody Account account) { 65 | Account createdAccount = accountService.createAccount(account); 66 | return new ResponseEntity<>(createdAccount, HttpStatus.CREATED); 67 | } 68 | 69 | @GetMapping("/all") 70 | public ResponseEntity> getAllAccounts() { 71 | List accounts = accountService.getAllAccounts(); 72 | return new ResponseEntity<>(accounts, HttpStatus.OK); 73 | } 74 | 75 | @GetMapping("/{id}") 76 | public ResponseEntity findAccountById(@PathVariable("id") Integer accountId) { 77 | Account account = accountService.findAccountById(accountId); 78 | if (account != null) { 79 | return new ResponseEntity<>(account, HttpStatus.OK); 80 | } else { 81 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 82 | } 83 | } 84 | 85 | @PutMapping("/{id}") 86 | public ResponseEntity updateAccount(@PathVariable("id") Integer accountId, @RequestBody Account account) { 87 | Account updatedAccount = accountService.updateAccount(accountId, account); 88 | if (updatedAccount != null) { 89 | return new ResponseEntity<>(updatedAccount, HttpStatus.OK); 90 | } else { 91 | return new ResponseEntity<>(HttpStatus.NOT_FOUND); 92 | } 93 | } 94 | 95 | @DeleteMapping("/{id}") 96 | public ResponseEntity deleteAccount(@PathVariable("id") Integer accountId) { 97 | accountService.deleteAccount(accountId); 98 | return new ResponseEntity<>(HttpStatus.NO_CONTENT); 99 | }*/ 100 | 101 | } -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/user/UserService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.Account; 4 | import com.amigoscode.group.ebankingsuite.account.AccountService; 5 | import com.amigoscode.group.ebankingsuite.exception.ResourceExistsException; 6 | import com.amigoscode.group.ebankingsuite.exception.ValueMismatchException; 7 | import com.amigoscode.group.ebankingsuite.user.requests.ChangePasswordRequest; 8 | import com.amigoscode.group.ebankingsuite.user.requests.UserAuthenticationRequests; 9 | import com.amigoscode.group.ebankingsuite.user.requests.UserRegistrationRequest; 10 | import com.amigoscode.group.ebankingsuite.config.JWTService; 11 | import com.amigoscode.group.ebankingsuite.exception.InvalidAuthenticationException; 12 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | import java.time.LocalDateTime; 19 | import java.util.Map; 20 | import java.util.Optional; 21 | 22 | @Service 23 | public class UserService { 24 | private final UserRepository userRepository; 25 | private final JWTService jwtService; 26 | private final AccountService accountService; 27 | private static final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); 28 | 29 | @Autowired 30 | public UserService(UserRepository userRepository, JWTService jwtService, AccountService accountService) { 31 | this.userRepository = userRepository; 32 | this.jwtService = jwtService; 33 | this.accountService = accountService; 34 | } 35 | 36 | 37 | public void updateUser(User existingUser){ 38 | existingUser.setUpdatedAt(LocalDateTime.now()); 39 | userRepository.save(existingUser); 40 | } 41 | 42 | 43 | /** 44 | * this service creates a new user provided the email does not exist and also invokes the account 45 | * service to create a new account for the user 46 | */ 47 | @Transactional 48 | public void createNewUser(UserRegistrationRequest userRegistrationRequest){ 49 | if(userRepository.existsByEmailAddress(userRegistrationRequest.emailAddress())){ 50 | throw new ResourceExistsException("email address is taken"); 51 | } 52 | User newUser = new User( 53 | userRegistrationRequest.fullName(), 54 | userRegistrationRequest.emailAddress(), 55 | bCryptPasswordEncoder.encode(userRegistrationRequest.password()), 56 | true, 57 | userRegistrationRequest.phoneNumber()); 58 | 59 | userRepository.save(newUser); 60 | accountService.createAccount(new Account(newUser.getId())); 61 | } 62 | 63 | public boolean passwordMatches(String rawPassword, String encodedPassword){ 64 | return bCryptPasswordEncoder.matches(rawPassword,encodedPassword); 65 | } 66 | 67 | 68 | public String authenticateUser(UserAuthenticationRequests requests){ 69 | User existingUser = getUserByEmailAddress(requests.emailAddress()); 70 | 71 | if (passwordMatches(requests.password(),existingUser.getPassword())){ 72 | Map claims = Map.of("userId", existingUser.getId()); 73 | return jwtService.generateToken(claims,existingUser); 74 | } 75 | throw new InvalidAuthenticationException("Invalid username or password"); 76 | 77 | } 78 | 79 | public void changeUserPassword(ChangePasswordRequest request, Integer userId){ 80 | User existingUser = getUserByUserId(userId); 81 | 82 | if(!bCryptPasswordEncoder.matches(request.oldPassword(),existingUser.getPassword())){ 83 | throw new ValueMismatchException("old password does not match"); 84 | } 85 | 86 | existingUser.setPassword(bCryptPasswordEncoder.encode(request.newPassword())); 87 | updateUser(existingUser); 88 | 89 | 90 | } 91 | 92 | public User getUserByUserId(int userId){ 93 | Optional existingUser = userRepository.findById(userId); 94 | if(existingUser.isEmpty()){ 95 | throw new ResourceNotFoundException("user not found"); 96 | } 97 | return existingUser.get(); 98 | } 99 | public User getUserByEmailAddress(String emailAddress){ 100 | Optional existingUser = userRepository.findByEmailAddress(emailAddress); 101 | if(existingUser.isEmpty()){ 102 | throw new ResourceNotFoundException("user not found"); 103 | } 104 | return existingUser.get(); 105 | } 106 | 107 | 108 | public String encodePassword(String password){ 109 | return bCryptPasswordEncoder.encode(password); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/user/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.AccountService; 4 | import com.amigoscode.group.ebankingsuite.user.requests.ChangePasswordRequest; 5 | import com.amigoscode.group.ebankingsuite.user.requests.UserAuthenticationRequests; 6 | import com.amigoscode.group.ebankingsuite.user.requests.UserRegistrationRequest; 7 | import com.amigoscode.group.ebankingsuite.config.JWTService; 8 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 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.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.mock.web.MockHttpServletRequest; 18 | import org.springframework.web.context.request.RequestContextHolder; 19 | import org.springframework.web.context.request.ServletRequestAttributes; 20 | 21 | import java.util.Map; 22 | import java.util.Optional; 23 | 24 | import static org.assertj.core.api.Java6Assertions.assertThat; 25 | import static org.mockito.BDDMockito.given; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class UserControllerTest { 29 | 30 | @Mock 31 | private UserRepository userRepository; 32 | @Mock 33 | private UserService userService; 34 | @Mock 35 | private JWTService jwtService; 36 | @Mock 37 | private AccountService accountService; 38 | 39 | private UserController userController; 40 | 41 | 42 | @BeforeEach 43 | public void setUp() { 44 | this.userService = new UserService(userRepository, jwtService, accountService); 45 | this.userController = new UserController(userService, jwtService); 46 | } 47 | 48 | @Test 49 | void willReturn202WhenSavingUser(){ 50 | //given 51 | UserRegistrationRequest userRequest = new UserRegistrationRequest( 52 | "lawal", 53 | "larwal@mail.com", 54 | "12345", 55 | "09074708156" 56 | ); 57 | MockHttpServletRequest request = new MockHttpServletRequest(); 58 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 59 | 60 | given(userRepository.existsByEmailAddress(userRequest.emailAddress())).willReturn(false); 61 | 62 | //when 63 | ResponseEntity responseEntity = userController.saveUser(userRequest); 64 | //then 65 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); 66 | } 67 | 68 | 69 | @Test 70 | void willReturn201WhenUsernameAndPasswordMatchForAuthentication(){ 71 | //given 72 | UserAuthenticationRequests authenticationRequests = new UserAuthenticationRequests( 73 | "larwal@mail.com", 74 | "12345" 75 | ); 76 | MockHttpServletRequest request = new MockHttpServletRequest(); 77 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 78 | User mockUser = new User( 79 | 1, 80 | "lawal Olakunle", 81 | "larwal@mail.com", 82 | userService.encodePassword("12345"), 83 | true); 84 | 85 | given(userRepository.findByEmailAddress(authenticationRequests.emailAddress())).willReturn(Optional.of(mockUser)); 86 | 87 | 88 | //when 89 | ResponseEntity responseEntity = userController.authenticateUser(authenticationRequests); 90 | 91 | //then 92 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 93 | } 94 | 95 | @Test 96 | void willReturn200WhenPasswordMatchForChangePassword(){ 97 | //given 98 | MockHttpServletRequest request = new MockHttpServletRequest(); 99 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 100 | ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest( 101 | "12345", 102 | "1234" 103 | ); 104 | 105 | Integer userId = 1; 106 | User mockUser = new User( 107 | 1, 108 | "lawal Olakunle", 109 | "larwal@mail.com", 110 | userService.encodePassword("12345"), 111 | true 112 | ); 113 | String jwt = "Bearer "+ "testToken"; 114 | 115 | given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); 116 | given(jwtService.extractUserIdFromToken(jwt)).willReturn(1); 117 | 118 | //when 119 | ResponseEntity responseEntity = 120 | userController.changeUserPassword(jwt,changePasswordRequest); 121 | //then 122 | assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200); 123 | } 124 | 125 | 126 | 127 | 128 | 129 | } -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/account/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.request.AccountTransactionPinUpdateModel; 4 | import com.amigoscode.group.ebankingsuite.config.JWTService; 5 | import com.amigoscode.group.ebankingsuite.universal.ApiResponse; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.mock.web.MockHttpServletRequest; 14 | import org.springframework.web.context.request.RequestContextHolder; 15 | import org.springframework.web.context.request.ServletRequestAttributes; 16 | 17 | import java.math.BigDecimal; 18 | import java.util.Optional; 19 | 20 | import static org.assertj.core.api.Java6Assertions.assertThat; 21 | import static org.mockito.BDDMockito.given; 22 | 23 | @ExtendWith(MockitoExtension.class) 24 | class AccountControllerTest { 25 | 26 | @Mock 27 | private AccountService accountService; 28 | @Mock 29 | private AccountRepository accountRepository; 30 | @Mock 31 | private JWTService jwtService; 32 | private AccountController accountController; 33 | 34 | @BeforeEach 35 | public void setUp() { 36 | this.accountService = new AccountService(accountRepository); 37 | this.accountController = new AccountController(accountService, jwtService); 38 | } 39 | 40 | @Test 41 | void willReturn200whenGettingUserAccountOverview() { 42 | //given 43 | MockHttpServletRequest request = new MockHttpServletRequest(); 44 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 45 | 46 | 47 | Integer userId = 1; 48 | Account newAccount = new Account( 49 | 1, 50 | new BigDecimal(0), 51 | AccountStatus.ACTIVATED, 52 | "6767576476", 53 | Tier.LEVEL1, 54 | "8493" 55 | ); 56 | String testJwt = "Bearer " + "testToken"; 57 | 58 | given(jwtService.extractUserIdFromToken(testJwt)).willReturn(1); 59 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(newAccount)); 60 | 61 | //when 62 | ResponseEntity responseEntity = 63 | accountController.getUserAccountOverview(testJwt); 64 | //then 65 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 66 | } 67 | 68 | @Test 69 | void willReturn404whenAccountNotFoundForUserIdWhenGeneratingAccountOverview() { 70 | //given 71 | MockHttpServletRequest request = new MockHttpServletRequest(); 72 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 73 | 74 | 75 | Integer userId = 1; 76 | String testJwt = "Bearer " + "testToken"; 77 | 78 | given(jwtService.extractUserIdFromToken(testJwt)).willReturn(1); 79 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.empty()); 80 | 81 | //when 82 | ResponseEntity responseEntity = 83 | accountController.getUserAccountOverview(testJwt); 84 | //then 85 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); 86 | } 87 | 88 | @Test 89 | void willReturn200whenClosingUserAccount() { 90 | //given 91 | MockHttpServletRequest request = new MockHttpServletRequest(); 92 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 93 | 94 | 95 | Integer userId = 1; 96 | Account newAccount = new Account( 97 | 1, 98 | new BigDecimal(0), 99 | AccountStatus.ACTIVATED, 100 | "6767576476", 101 | Tier.LEVEL1, 102 | "8493" 103 | ); 104 | String testJwt = "Bearer " + "testToken"; 105 | 106 | given(jwtService.extractUserIdFromToken(testJwt)).willReturn(1); 107 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(newAccount)); 108 | 109 | //when 110 | ResponseEntity responseEntity = 111 | accountController.closeAccount(testJwt); 112 | //then 113 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 114 | } 115 | 116 | 117 | 118 | @Test 119 | void willReturn200WhenUpdatingTransactionPin() { 120 | //given 121 | MockHttpServletRequest request = new MockHttpServletRequest(); 122 | RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); 123 | 124 | AccountTransactionPinUpdateModel pinUpdateModel = 125 | new AccountTransactionPinUpdateModel("1234"); 126 | Integer userId = 1; 127 | Account existingAccount = new Account( 128 | 1, 129 | new BigDecimal(0), 130 | AccountStatus.ACTIVATED, 131 | "6767576476", 132 | Tier.LEVEL1, 133 | "8493" 134 | ); 135 | String testJwt = "Bearer " + "testToken"; 136 | 137 | given(jwtService.extractUserIdFromToken(testJwt)).willReturn(1); 138 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(existingAccount)); 139 | 140 | //when 141 | ResponseEntity responseEntity = 142 | accountController.updateAccountTransactionPin(testJwt,pinUpdateModel); 143 | //then 144 | assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); 145 | } 146 | 147 | 148 | } -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/account/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.request.AccountTransactionPinUpdateModel; 4 | import com.amigoscode.group.ebankingsuite.account.response.AccountOverviewResponse; 5 | import com.amigoscode.group.ebankingsuite.exception.AccountNotActivatedException; 6 | import com.amigoscode.group.ebankingsuite.exception.AccountNotClearedException; 7 | import com.amigoscode.group.ebankingsuite.exception.InsufficientBalanceException; 8 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.scheduling.annotation.Async; 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 12 | import org.springframework.stereotype.Service; 13 | import java.math.BigDecimal; 14 | import java.time.LocalDateTime; 15 | import java.util.Optional; 16 | import java.util.concurrent.ThreadLocalRandom; 17 | 18 | @Service 19 | public class AccountService { 20 | 21 | private final AccountRepository accountRepository; 22 | private static final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); 23 | 24 | @Autowired 25 | public AccountService(AccountRepository accountRepository) { 26 | this.accountRepository = accountRepository; 27 | } 28 | 29 | @Async 30 | public void createAccount(Account account) { 31 | account.setAccountNumber(generateUniqueAccountNumber()); 32 | account.setTierLevel(Tier.LEVEL1); 33 | account.setAccountStatus(AccountStatus.ACTIVATED); 34 | accountRepository.save(account); 35 | } 36 | 37 | /** 38 | * This method generates random 10 digit values and convert to string 39 | * for use as account number for accounts 40 | */ 41 | private String generateUniqueAccountNumber() { 42 | ThreadLocalRandom random = ThreadLocalRandom.current(); 43 | int accountNumber; 44 | boolean exists; 45 | do { 46 | accountNumber = random.nextInt(1_000_000_000); 47 | exists = accountRepository.existsByAccountNumber(String.format("%09d", accountNumber)); 48 | } while (exists); 49 | return String.format("%09d", accountNumber); 50 | } 51 | 52 | public Account getAccountByUserId(Integer userId) { 53 | Optional account = accountRepository.findAccountByUserId(userId); 54 | if(account.isEmpty()){ 55 | throw new ResourceNotFoundException("account not found"); 56 | } 57 | return account.get(); 58 | } 59 | 60 | /** 61 | *Generates basic account overview (i.e. balance, accountNumber, tierLevel, accountStatus)of the user and receives userId 62 | */ 63 | public AccountOverviewResponse generateAccountOverviewByUserId(Integer userId){ 64 | Account userAccount = getAccountByUserId(userId); 65 | return new AccountOverviewResponse( 66 | userAccount.getAccountBalance(), 67 | userAccount.getAccountNumber(), 68 | userAccount.getTierLevel().name(), 69 | userAccount.getAccountStatus().name() 70 | ); 71 | } 72 | 73 | public void updateAccount(Account existingAccount) { 74 | existingAccount.setUpdatedAt(LocalDateTime.now()); 75 | accountRepository.save(existingAccount); 76 | } 77 | 78 | /** 79 | * This method closes the account by getting the userId from the JWT and the relieving reason 80 | * from the request body 81 | */ 82 | public void closeAccount(Integer userId){ 83 | Account userAccount = getAccountByUserId(userId); 84 | if(!noPendingOrAvailableFundInTheAccount(userAccount)) { 85 | throw new AccountNotClearedException("confirm there is no pending or available balance in the account"); 86 | } 87 | userAccount.setAccountStatus(AccountStatus.CLOSED); 88 | updateAccount(userAccount); 89 | } 90 | 91 | private boolean noPendingOrAvailableFundInTheAccount(Account account){ 92 | return account.getAccountBalance().equals(BigDecimal.ZERO); 93 | } 94 | 95 | /** 96 | * This confirms the pin is 4-digits, more pin standards can be set here 97 | */ 98 | private boolean pinConformsToStandard(String transactionPin){ 99 | return transactionPin.length() == 4; 100 | } 101 | 102 | public void updateAccountTransactionPin(int userId,AccountTransactionPinUpdateModel pinUpdateModel){ 103 | Account userAccount = getAccountByUserId(userId); 104 | if(!pinConformsToStandard(pinUpdateModel.transactionPin())){ 105 | throw new IllegalArgumentException("Bad transaction pin format"); 106 | } 107 | userAccount.setTransactionPin(bCryptPasswordEncoder.encode(pinUpdateModel.transactionPin())); 108 | updateAccount(userAccount); 109 | } 110 | public void creditAccount(Account receiverAccount,BigDecimal amount) { 111 | receiverAccount.setAccountBalance(receiverAccount.getAccountBalance().add(amount)); 112 | updateAccount(receiverAccount); 113 | } 114 | public Account accountExistsAndIsActivated(String accountNumber){ 115 | Optional exitingAccount = accountRepository.findAccountByAccountNumber(accountNumber); 116 | if(exitingAccount.isPresent()){ 117 | if(exitingAccount.get().getAccountStatus().equals(AccountStatus.ACTIVATED)){ 118 | return exitingAccount.get(); 119 | } 120 | throw new AccountNotActivatedException("Account not activated"); 121 | } 122 | throw new ResourceNotFoundException("Account not found"); 123 | } 124 | public void debitAccount(Account senderAccount, BigDecimal amount) { 125 | 126 | if(senderAccount.getAccountBalance().compareTo(amount)<=0) { 127 | throw new InsufficientBalanceException("Insufficient funds"); 128 | } 129 | senderAccount.setAccountBalance(senderAccount.getAccountBalance().subtract(amount)); 130 | updateAccount(senderAccount); 131 | } 132 | } -------------------------------------------------------------------------------- /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 "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\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/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq 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%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.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% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/user/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.user; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.AccountService; 4 | import com.amigoscode.group.ebankingsuite.config.JWTService; 5 | import com.amigoscode.group.ebankingsuite.exception.ResourceExistsException; 6 | import com.amigoscode.group.ebankingsuite.exception.ValueMismatchException; 7 | import com.amigoscode.group.ebankingsuite.user.requests.ChangePasswordRequest; 8 | import com.amigoscode.group.ebankingsuite.user.requests.UserAuthenticationRequests; 9 | import com.amigoscode.group.ebankingsuite.user.requests.UserRegistrationRequest; 10 | import com.amigoscode.group.ebankingsuite.exception.InvalidAuthenticationException; 11 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.ArgumentCaptor; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | 20 | import java.util.Map; 21 | import java.util.Optional; 22 | 23 | import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.mockito.Mockito.verify; 26 | import static org.assertj.core.api.Java6Assertions.assertThat; 27 | 28 | 29 | 30 | @ExtendWith(MockitoExtension.class) 31 | class UserServiceTest { 32 | 33 | private UserService userService; 34 | @Mock 35 | private UserRepository userRepository; 36 | @Mock 37 | private JWTService jwtService; 38 | @Mock 39 | private AccountService accountService; 40 | 41 | 42 | @BeforeEach 43 | public void setUp() { 44 | this.userService = new UserService(userRepository, jwtService, accountService); 45 | } 46 | 47 | @Test 48 | void canSaveUserWhenEmailAddressIsUnique(){ 49 | //given 50 | UserRegistrationRequest testRequest = new UserRegistrationRequest( 51 | "pen tami", 52 | "pentami@mailer.com", 53 | "12345", 54 | "+2349087708156" 55 | ); 56 | given(userRepository.existsByEmailAddress(testRequest.emailAddress())).willReturn(false); 57 | 58 | //when 59 | userService.createNewUser(testRequest); 60 | 61 | //then 62 | ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); 63 | verify(userRepository).save(userArgumentCaptor.capture()); 64 | assertThat(userArgumentCaptor.getValue()).isEqualToComparingOnlyGivenFields(new User( 65 | testRequest.fullName(),testRequest.emailAddress(),testRequest.password(),true,testRequest.phoneNumber()), 66 | "fullName","emailAddress","isNotBlocked"); 67 | } 68 | 69 | @Test 70 | void willThrowResourceExistsExceptionWhenEmailAddressIsNotUniqueWhileSavingUser(){ 71 | //given 72 | UserRegistrationRequest testRequest = new UserRegistrationRequest( 73 | "pen tami", 74 | "pentami@mailer.com", 75 | "12345", 76 | "+234907656788" 77 | ); 78 | given(userRepository.existsByEmailAddress(testRequest.emailAddress())).willReturn(true); 79 | 80 | //when 81 | //that 82 | System.out.println("here"); 83 | assertThatThrownBy(()-> userService.createNewUser(testRequest)) 84 | .isInstanceOf(ResourceExistsException.class) 85 | .hasMessage("email address is taken"); 86 | } 87 | 88 | @Test 89 | void canAuthenticateUserEmailAndPasswordMatch(){ 90 | //given 91 | UserAuthenticationRequests authenticationRequests = new UserAuthenticationRequests( 92 | "larwal@mail.com", 93 | "12345" 94 | ); 95 | User mockUser = new User( 96 | 1, 97 | "lawal Olakunle", 98 | "larwal@mail.com", 99 | userService.encodePassword("12345"), 100 | true); 101 | given(userRepository.findByEmailAddress( 102 | authenticationRequests.emailAddress())).willReturn(Optional.of(mockUser)); 103 | 104 | //when 105 | userService.authenticateUser(authenticationRequests); 106 | 107 | //then 108 | ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); 109 | ArgumentCaptor mapArgumentCaptor = ArgumentCaptor.forClass(Map.class); 110 | verify(jwtService).generateToken(mapArgumentCaptor.capture(),userArgumentCaptor.capture()); 111 | assertThat(userArgumentCaptor.getValue()).isEqualToComparingOnlyGivenFields(new User( 112 | mockUser.getFullName(),mockUser.getEmailAddress(),mockUser.getPassword(),true,"+234658789"), 113 | "fullName","emailAddress","isNotBlocked"); 114 | } 115 | @Test 116 | void willThrowInvalidAuthenticationExceptionWhenEmailAndPasswordDoesNotMatchWhenAuthenticatingUser(){ 117 | //given 118 | UserAuthenticationRequests authenticationRequests = new UserAuthenticationRequests( 119 | "larwal@mail.com", 120 | "12345" 121 | ); 122 | User mockUser = new User( 123 | "lawal Olakunle", 124 | "larwal@mail.com", 125 | userService.encodePassword("1234"), 126 | true, 127 | "+2346987898"); 128 | 129 | given(userRepository.findByEmailAddress( 130 | authenticationRequests.emailAddress())).willReturn(Optional.of(mockUser)); 131 | 132 | //when 133 | //that 134 | assertThatThrownBy(()-> userService.authenticateUser(authenticationRequests)) 135 | .isInstanceOf(InvalidAuthenticationException.class) 136 | .hasMessage("Invalid username or password"); 137 | } 138 | 139 | @Test 140 | void willThrowResourceNotFoundExceptionWhenEmailDoesNotExistsWhileAuthenticatingUser(){ 141 | //given 142 | UserAuthenticationRequests authenticationRequests = new UserAuthenticationRequests( 143 | "larwal@mail.com", 144 | "12345" 145 | ); 146 | 147 | given(userRepository.findByEmailAddress( 148 | authenticationRequests.emailAddress())).willReturn(Optional.empty()); 149 | 150 | //when 151 | //that 152 | assertThatThrownBy(()-> userService.authenticateUser(authenticationRequests)) 153 | .isInstanceOf(ResourceNotFoundException.class) 154 | .hasMessage("user not found"); 155 | } 156 | 157 | @Test 158 | void canChangeUserPassword(){ 159 | //given 160 | ChangePasswordRequest request = new ChangePasswordRequest( 161 | "12345", 162 | "1234" 163 | ); 164 | Integer userId = 1; 165 | User mockUser = new User( 166 | 1, 167 | "lawal Olakunle", 168 | "larwal@mail.com", 169 | userService.encodePassword("12345"), 170 | true); 171 | 172 | given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); 173 | 174 | //when 175 | userService.changeUserPassword(request,userId); 176 | 177 | //then 178 | ArgumentCaptor userArgumentCaptor = ArgumentCaptor.forClass(User.class); 179 | verify(userRepository).save(userArgumentCaptor.capture()); 180 | assertThat(userService.passwordMatches(request.newPassword(),userArgumentCaptor.getValue().getPassword())).isTrue(); 181 | } 182 | 183 | @Test 184 | void willThrowValueMismatchExceptionWhenOldPasswordDoesNotMatchWhenChangingPassword(){ 185 | //given 186 | ChangePasswordRequest request = new ChangePasswordRequest( 187 | "1245", 188 | "1234" 189 | ); 190 | Integer userId = 1; 191 | User mockUser = new User( 192 | 1, 193 | "lawal Olakunle", 194 | "larwal@mail.com", 195 | userService.encodePassword("12345"), 196 | true 197 | ); 198 | given(userRepository.findById(userId)).willReturn(Optional.of(mockUser)); 199 | 200 | //when 201 | //then 202 | assertThatThrownBy(() ->userService.changeUserPassword(request,userId)) 203 | .isInstanceOf(ValueMismatchException.class) 204 | .hasMessage("old password does not match"); 205 | } 206 | @Test 207 | void willThrowResourceNotFoundExceptionWhenUserNotFoundForUserIdWhenChangingPassword(){ 208 | //given 209 | ChangePasswordRequest request = new ChangePasswordRequest( 210 | "1245", 211 | "1234" 212 | ); 213 | Integer userId = 1; 214 | 215 | given(userRepository.findById(userId)).willReturn(Optional.empty()); 216 | 217 | //when 218 | //then 219 | assertThatThrownBy(() ->userService.changeUserPassword(request,userId)) 220 | .isInstanceOf(ResourceNotFoundException.class) 221 | .hasMessage("user not found"); 222 | } 223 | 224 | } -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/transaction/TransactionServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.*; 4 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 5 | import com.amigoscode.group.ebankingsuite.exception.ValueMismatchException; 6 | import com.amigoscode.group.ebankingsuite.notification.NotificationSenderService; 7 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.EmailSenderService; 8 | import com.amigoscode.group.ebankingsuite.transaction.request.FundsTransferRequest; 9 | import com.amigoscode.group.ebankingsuite.transaction.request.TransactionHistoryRequest; 10 | import com.amigoscode.group.ebankingsuite.transaction.response.TransactionHistoryResponse; 11 | import com.amigoscode.group.ebankingsuite.transaction.response.TransactionType; 12 | import com.amigoscode.group.ebankingsuite.user.User; 13 | import com.amigoscode.group.ebankingsuite.user.UserService; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | import org.springframework.data.domain.*; 21 | import java.math.BigDecimal; 22 | import java.time.LocalDateTime; 23 | import java.util.List; 24 | import static org.assertj.core.api.Java6Assertions.assertThat; 25 | import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; 26 | import static org.mockito.BDDMockito.given; 27 | import static org.mockito.Mockito.*; 28 | 29 | @ExtendWith(MockitoExtension.class) 30 | class TransactionServiceTest { 31 | 32 | @Mock 33 | private TransactionRepository transactionRepository; 34 | @Mock 35 | private AccountService accountService; 36 | @Mock 37 | private UserService userService; 38 | @Mock 39 | private NotificationSenderService emailNotificationService; 40 | @InjectMocks 41 | private TransactionService transactionService; 42 | 43 | @BeforeEach 44 | void setUp() { 45 | transactionService = new TransactionService(transactionRepository, accountService, userService, emailNotificationService); 46 | } 47 | 48 | @Test 49 | void canSuccessfullyTransferFunds() { 50 | //given 51 | FundsTransferRequest request = new FundsTransferRequest("165568799", "986562737", new BigDecimal(200), "1234", "test transfer"); 52 | 53 | Account senderAccount = new Account(1,new BigDecimal(200), AccountStatus.ACTIVATED,"986562737", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 54 | Account receiverAccount = new Account(2,new BigDecimal(0), AccountStatus.ACTIVATED,"165568799", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 55 | 56 | User senderUser = new User(1, "test Name 1", "test1@mail.com", userService.encodePassword("12345"), true); 57 | User reveiverUser = new User(2, "test Name 2", "test2@mail.com", userService.encodePassword("12345"), true); 58 | 59 | given(accountService.accountExistsAndIsActivated("986562737")).willReturn(senderAccount); 60 | given(accountService.accountExistsAndIsActivated("165568799")).willReturn(receiverAccount); 61 | given(userService.getUserByUserId(1)).willReturn(senderUser); 62 | given(userService.getUserByUserId(2)).willReturn(reveiverUser); 63 | 64 | //when 65 | transactionService.transferFunds(request); 66 | 67 | //then 68 | verify(accountService, times(1)).debitAccount(senderAccount, request.amount()); 69 | verify(accountService, times(1)).creditAccount(receiverAccount, request.amount()); 70 | } 71 | 72 | @Test 73 | void willThrowValueMismatch_TransferFunds_TransactionPinMismatch(){ 74 | //given 75 | FundsTransferRequest request = new FundsTransferRequest("165568799", "986562737", new BigDecimal(200), "1224", "test transfer"); 76 | Account senderAccount = new Account(1,new BigDecimal(200), AccountStatus.ACTIVATED,"986562737", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 77 | given(accountService.accountExistsAndIsActivated("986562737")).willReturn(senderAccount); 78 | 79 | //when 80 | //then 81 | assertThatThrownBy(()-> transactionService.transferFunds(request)) 82 | .isInstanceOf(ValueMismatchException.class) 83 | .hasMessage("incorrect transaction pin"); 84 | } 85 | 86 | @Test 87 | void will_Throw_IllegalArgumentException_For_TransferFunds_When_SenderAndReceiverAccountNumberIsTheSame(){ 88 | //given 89 | FundsTransferRequest request = new FundsTransferRequest("165568799", "165568799", new BigDecimal(200), "1224", "test transfer"); 90 | 91 | //when 92 | //then 93 | assertThatThrownBy(()-> transactionService.transferFunds(request)) 94 | .isInstanceOf(IllegalArgumentException.class) 95 | .hasMessage("sender account cannot be recipient account"); 96 | } 97 | 98 | void can_generate_transaction_history_when_all_params_are_valid(){ 99 | //given 100 | Account senderAccount = new Account(1,new BigDecimal(200), AccountStatus.ACTIVATED,"986562737", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 101 | 102 | } 103 | @Test 104 | void can_confirm_transaction_type_is_Credit(){ 105 | //given 106 | Transaction transaction = new Transaction("878676790", "765362789", new BigDecimal("500"), "testRefNum", "testTransaction", TransactionStatus.SUCCESS, "testSender", "testReceiver"); 107 | String userAccountNumber = "765362789"; 108 | 109 | //when 110 | TransactionType actualOutput = transactionService.checkTransactionType(transaction, userAccountNumber); 111 | 112 | //then 113 | assertThat(actualOutput).isEqualTo(TransactionType.CREDIT); 114 | } 115 | 116 | @Test 117 | void can_Confirm_Transaction_Type_Is_Debit(){ 118 | //given 119 | Transaction transaction = new Transaction("878676790", "765362789", new BigDecimal("500"), "testRefNum", "testTransaction", TransactionStatus.SUCCESS, "testSender", "testReceiver"); 120 | String userAccountNumber = "878676790"; 121 | 122 | //when 123 | TransactionType actualOutput = transactionService.checkTransactionType(transaction, userAccountNumber); 124 | 125 | //then 126 | assertThat(actualOutput).isEqualTo(TransactionType.DEBIT); 127 | } 128 | 129 | @Test 130 | void will_throw_IllegalArgumentExceptionWhen_Transaction_Type_Cannot_Be_Determined(){ 131 | //given 132 | Transaction transaction = new Transaction("878676790", "765362789", new BigDecimal("500"), "testRefNum", "testTransaction", TransactionStatus.SUCCESS, "testSender", "testReceiver"); 133 | String userAccountNumber = "878676793"; 134 | 135 | //when 136 | //then 137 | assertThatThrownBy(()-> transactionService.checkTransactionType(transaction, userAccountNumber)) 138 | .isInstanceOf(IllegalArgumentException.class) 139 | .hasMessage("error processing cannot determine transaction type"); 140 | } 141 | 142 | @Test 143 | void can_Generate_Transaction_History_Between_Dates_by_userId(){ 144 | //given 145 | int userId = 1; 146 | Account userAccount = new Account(1,new BigDecimal(200), AccountStatus.ACTIVATED,"986562737", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 147 | Page transactions = 148 | new PageImpl<>(List.of(new Transaction("986562737", "765362789", new BigDecimal("500"), "testRefNum", "testTransaction", TransactionStatus.SUCCESS, "testSender", "testReceiver"))); 149 | 150 | TransactionHistoryRequest request = new TransactionHistoryRequest(LocalDateTime.now(),LocalDateTime.now().plusHours(2L)); 151 | 152 | given(accountService.getAccountByUserId(userId)).willReturn(userAccount); 153 | given(transactionRepository.findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 154 | TransactionStatus.SUCCESS, 155 | request.startDateTime(), 156 | request.endDateTime(), 157 | "986562737", 158 | "986562737", 159 | PageRequest.of(1,1) 160 | )).willReturn(transactions); 161 | 162 | //when 163 | //then 164 | assertThat(transactionService.getTransactionHistoryByUserId(request, userId, PageRequest.of(1,1)).get(0)) 165 | .isInstanceOf(TransactionHistoryResponse.class); 166 | 167 | } 168 | 169 | @Test 170 | void can_Throw_ResourceNotFoundException_When_Transaction_Is_Empty_When_Generating_Transaction_History_Between_Dates_by_userId(){ 171 | //given 172 | int userId = 1; 173 | Account userAccount = new Account(1,new BigDecimal(200), AccountStatus.ACTIVATED,"986562737", Tier.LEVEL1,"$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa"); 174 | Page transactions = new PageImpl<>(List.of()); 175 | 176 | TransactionHistoryRequest request = new TransactionHistoryRequest(LocalDateTime.now(),LocalDateTime.now().plusHours(2L)); 177 | 178 | given(accountService.getAccountByUserId(userId)).willReturn(userAccount); 179 | given(transactionRepository.findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 180 | TransactionStatus.SUCCESS, 181 | request.startDateTime(), 182 | request.endDateTime(), 183 | "986562737", 184 | "986562737", 185 | PageRequest.of(1,1) 186 | )).willReturn(transactions); 187 | 188 | //when 189 | //then 190 | assertThatThrownBy(()->transactionService.getTransactionHistoryByUserId(request, userId, PageRequest.of(1,1))) 191 | .isInstanceOf(ResourceNotFoundException.class) 192 | .hasMessage("no transactions"); 193 | 194 | } 195 | } -------------------------------------------------------------------------------- /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 /usr/local/etc/mavenrc ] ; then 40 | . /usr/local/etc/mavenrc 41 | fi 42 | 43 | if [ -f /etc/mavenrc ] ; then 44 | . /etc/mavenrc 45 | fi 46 | 47 | if [ -f "$HOME/.mavenrc" ] ; then 48 | . "$HOME/.mavenrc" 49 | fi 50 | 51 | fi 52 | 53 | # OS specific support. $var _must_ be set to either true or false. 54 | cygwin=false; 55 | darwin=false; 56 | mingw=false 57 | case "`uname`" in 58 | CYGWIN*) cygwin=true ;; 59 | MINGW*) mingw=true;; 60 | Darwin*) darwin=true 61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 63 | if [ -z "$JAVA_HOME" ]; then 64 | if [ -x "/usr/libexec/java_home" ]; then 65 | export JAVA_HOME="`/usr/libexec/java_home`" 66 | else 67 | export JAVA_HOME="/Library/Java/Home" 68 | fi 69 | fi 70 | ;; 71 | esac 72 | 73 | if [ -z "$JAVA_HOME" ] ; then 74 | if [ -r /etc/gentoo-release ] ; then 75 | JAVA_HOME=`java-config --jre-home` 76 | fi 77 | fi 78 | 79 | if [ -z "$M2_HOME" ] ; then 80 | ## resolve links - $0 may be a link to maven's home 81 | PRG="$0" 82 | 83 | # need this for relative symlinks 84 | while [ -h "$PRG" ] ; do 85 | ls=`ls -ld "$PRG"` 86 | link=`expr "$ls" : '.*-> \(.*\)$'` 87 | if expr "$link" : '/.*' > /dev/null; then 88 | PRG="$link" 89 | else 90 | PRG="`dirname "$PRG"`/$link" 91 | fi 92 | done 93 | 94 | saveddir=`pwd` 95 | 96 | M2_HOME=`dirname "$PRG"`/.. 97 | 98 | # make it fully qualified 99 | M2_HOME=`cd "$M2_HOME" && pwd` 100 | 101 | cd "$saveddir" 102 | # echo Using m2 at $M2_HOME 103 | fi 104 | 105 | # For Cygwin, ensure paths are in UNIX format before anything is touched 106 | if $cygwin ; then 107 | [ -n "$M2_HOME" ] && 108 | M2_HOME=`cygpath --unix "$M2_HOME"` 109 | [ -n "$JAVA_HOME" ] && 110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 111 | [ -n "$CLASSPATH" ] && 112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 113 | fi 114 | 115 | # For Mingw, ensure paths are in UNIX format before anything is touched 116 | if $mingw ; then 117 | [ -n "$M2_HOME" ] && 118 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 119 | [ -n "$JAVA_HOME" ] && 120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 121 | fi 122 | 123 | if [ -z "$JAVA_HOME" ]; then 124 | javaExecutable="`which javac`" 125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 126 | # readlink(1) is not available as standard on Solaris 10. 127 | readLink=`which readlink` 128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 129 | if $darwin ; then 130 | javaHome="`dirname \"$javaExecutable\"`" 131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 132 | else 133 | javaExecutable="`readlink -f \"$javaExecutable\"`" 134 | fi 135 | javaHome="`dirname \"$javaExecutable\"`" 136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 137 | JAVA_HOME="$javaHome" 138 | export JAVA_HOME 139 | fi 140 | fi 141 | fi 142 | 143 | if [ -z "$JAVACMD" ] ; then 144 | if [ -n "$JAVA_HOME" ] ; then 145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 146 | # IBM's JDK on AIX uses strange locations for the executables 147 | JAVACMD="$JAVA_HOME/jre/sh/java" 148 | else 149 | JAVACMD="$JAVA_HOME/bin/java" 150 | fi 151 | else 152 | JAVACMD="`\\unset -f command; \\command -v java`" 153 | fi 154 | fi 155 | 156 | if [ ! -x "$JAVACMD" ] ; then 157 | echo "Error: JAVA_HOME is not defined correctly." >&2 158 | echo " We cannot execute $JAVACMD" >&2 159 | exit 1 160 | fi 161 | 162 | if [ -z "$JAVA_HOME" ] ; then 163 | echo "Warning: JAVA_HOME environment variable is not set." 164 | fi 165 | 166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 167 | 168 | # traverses directory structure from process work directory to filesystem root 169 | # first directory with .mvn subdirectory is considered project base directory 170 | find_maven_basedir() { 171 | 172 | if [ -z "$1" ] 173 | then 174 | echo "Path not specified to find_maven_basedir" 175 | return 1 176 | fi 177 | 178 | basedir="$1" 179 | wdir="$1" 180 | while [ "$wdir" != '/' ] ; do 181 | if [ -d "$wdir"/.mvn ] ; then 182 | basedir=$wdir 183 | break 184 | fi 185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 186 | if [ -d "${wdir}" ]; then 187 | wdir=`cd "$wdir/.."; pwd` 188 | fi 189 | # end of workaround 190 | done 191 | echo "${basedir}" 192 | } 193 | 194 | # concatenates all lines of a file 195 | concat_lines() { 196 | if [ -f "$1" ]; then 197 | echo "$(tr -s '\n' ' ' < "$1")" 198 | fi 199 | } 200 | 201 | BASE_DIR=`find_maven_basedir "$(pwd)"` 202 | if [ -z "$BASE_DIR" ]; then 203 | exit 1; 204 | fi 205 | 206 | ########################################################################################## 207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 208 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 209 | ########################################################################################## 210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Found .mvn/wrapper/maven-wrapper.jar" 213 | fi 214 | else 215 | if [ "$MVNW_VERBOSE" = true ]; then 216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 217 | fi 218 | if [ -n "$MVNW_REPOURL" ]; then 219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 220 | else 221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 222 | fi 223 | while IFS="=" read key value; do 224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 225 | esac 226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 227 | if [ "$MVNW_VERBOSE" = true ]; then 228 | echo "Downloading from: $jarUrl" 229 | fi 230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 231 | if $cygwin; then 232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 233 | fi 234 | 235 | if command -v wget > /dev/null; then 236 | if [ "$MVNW_VERBOSE" = true ]; then 237 | echo "Found wget ... using wget" 238 | fi 239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 241 | else 242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" 243 | fi 244 | elif command -v curl > /dev/null; then 245 | if [ "$MVNW_VERBOSE" = true ]; then 246 | echo "Found curl ... using curl" 247 | fi 248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 249 | curl -o "$wrapperJarPath" "$jarUrl" -f 250 | else 251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 252 | fi 253 | 254 | else 255 | if [ "$MVNW_VERBOSE" = true ]; then 256 | echo "Falling back to using Java to download" 257 | fi 258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 259 | # For Cygwin, switch paths to Windows format before running javac 260 | if $cygwin; then 261 | javaClass=`cygpath --path --windows "$javaClass"` 262 | fi 263 | if [ -e "$javaClass" ]; then 264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 265 | if [ "$MVNW_VERBOSE" = true ]; then 266 | echo " - Compiling MavenWrapperDownloader.java ..." 267 | fi 268 | # Compiling the Java class 269 | ("$JAVA_HOME/bin/javac" "$javaClass") 270 | fi 271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 272 | # Running the downloader 273 | if [ "$MVNW_VERBOSE" = true ]; then 274 | echo " - Running MavenWrapperDownloader.java ..." 275 | fi 276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 277 | fi 278 | fi 279 | fi 280 | fi 281 | ########################################################################################## 282 | # End of extension 283 | ########################################################################################## 284 | 285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 286 | if [ "$MVNW_VERBOSE" = true ]; then 287 | echo $MAVEN_PROJECTBASEDIR 288 | fi 289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 290 | 291 | # For Cygwin, switch paths to Windows format before running java 292 | if $cygwin; then 293 | [ -n "$M2_HOME" ] && 294 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 295 | [ -n "$JAVA_HOME" ] && 296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 297 | [ -n "$CLASSPATH" ] && 298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 299 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 301 | fi 302 | 303 | # Provide a "standardized" way to retrieve the CLI args that will 304 | # work with both Windows and non-Windows executions. 305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 306 | export MAVEN_CMD_LINE_ARGS 307 | 308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 309 | 310 | exec "$JAVACMD" \ 311 | $MAVEN_OPTS \ 312 | $MAVEN_DEBUG_OPTS \ 313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 314 | "-Dmaven.home=${M2_HOME}" \ 315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 317 | -------------------------------------------------------------------------------- /src/main/java/com/amigoscode/group/ebankingsuite/transaction/TransactionService.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.transaction; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.Account; 4 | import com.amigoscode.group.ebankingsuite.account.AccountService; 5 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 6 | import com.amigoscode.group.ebankingsuite.exception.ValueMismatchException; 7 | import com.amigoscode.group.ebankingsuite.notification.NotificationSenderService; 8 | import com.amigoscode.group.ebankingsuite.notification.emailNotification.request.FundsAlertNotificationRequest; 9 | import com.amigoscode.group.ebankingsuite.transaction.request.FundsTransferRequest; 10 | import com.amigoscode.group.ebankingsuite.transaction.request.TransactionHistoryRequest; 11 | import com.amigoscode.group.ebankingsuite.transaction.response.TransactionHistoryResponse; 12 | import com.amigoscode.group.ebankingsuite.transaction.response.TransactionType; 13 | import com.amigoscode.group.ebankingsuite.user.UserService; 14 | import com.itextpdf.text.*; 15 | import com.itextpdf.text.pdf.PdfPCell; 16 | import com.itextpdf.text.pdf.PdfPTable; 17 | import com.itextpdf.text.pdf.PdfWriter; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.data.domain.Pageable; 20 | import org.springframework.data.domain.Slice; 21 | import org.springframework.scheduling.annotation.Async; 22 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 23 | import org.springframework.stereotype.Service; 24 | import org.springframework.transaction.annotation.Transactional; 25 | import java.io.ByteArrayOutputStream; 26 | import java.security.SecureRandom; 27 | import java.time.format.DateTimeFormatter; 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | 32 | 33 | @Service 34 | public class TransactionService { 35 | 36 | private final TransactionRepository transactionRepository; 37 | private final AccountService accountService; 38 | private final UserService userService; 39 | private final NotificationSenderService notificationSenderService; 40 | 41 | private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(); 42 | private static final SecureRandom SECURE_RANDOM = new SecureRandom(); 43 | 44 | 45 | @Autowired 46 | public TransactionService(TransactionRepository transactionRepository, AccountService accountService, UserService userService, NotificationSenderService notificationSenderService) { 47 | this.transactionRepository = transactionRepository; 48 | this.accountService = accountService; 49 | this.userService = userService; 50 | this.notificationSenderService = notificationSenderService; 51 | } 52 | 53 | 54 | @Transactional 55 | public void transferFunds(FundsTransferRequest request){ 56 | if(!request.senderAccountNumber().equals(request.receiverAccountNumber())){ 57 | Account senderAccount = accountService.accountExistsAndIsActivated(request.senderAccountNumber()); 58 | if(ENCODER.matches(request.transactionPin(), senderAccount.getTransactionPin())) { 59 | Account receiverAccount = accountService.accountExistsAndIsActivated(request.receiverAccountNumber()); 60 | accountService.debitAccount(senderAccount, request.amount()); 61 | accountService.creditAccount(receiverAccount, request.amount()); 62 | saveNewTransaction(request, senderAccount, receiverAccount); 63 | notificationSenderService.sendCreditAndDebitNotification(new FundsAlertNotificationRequest(senderAccount.getUserId(),receiverAccount.getUserId(),senderAccount.getAccountBalance(),receiverAccount.getAccountBalance(),request.amount())); 64 | return; 65 | } 66 | throw new ValueMismatchException("incorrect transaction pin"); 67 | } 68 | throw new IllegalArgumentException("sender account cannot be recipient account"); 69 | } 70 | 71 | /** 72 | * This method save a new transaction after completion, it is an asynchronous process 73 | * because the method using it doesn't need it response 74 | */ 75 | @Async 76 | public void saveNewTransaction(FundsTransferRequest request, Account senderAccount, Account receiverAccount){ 77 | 78 | transactionRepository.save( 79 | new Transaction(request.senderAccountNumber(), 80 | request.receiverAccountNumber(), 81 | request.amount(), 82 | generateTransactionReference(), 83 | request.narration(), 84 | TransactionStatus.SUCCESS, 85 | userService.getUserByUserId(senderAccount.getUserId()).getFullName(), 86 | userService.getUserByUserId(receiverAccount.getUserId()).getFullName()) 87 | ); 88 | } 89 | 90 | /** 91 | * generates random reference number it keeps generating until it gets a unique value. 92 | */ 93 | private String generateTransactionReference(){ 94 | final String VALUES = "abcdefghijklmnopqrstuvwxyz0123456789"; 95 | final int referenceNumberLength = 12; 96 | StringBuilder builder = new StringBuilder(referenceNumberLength); 97 | do { 98 | for (int i = 0; i < referenceNumberLength; i++) { 99 | builder.append(VALUES.charAt(SECURE_RANDOM.nextInt(VALUES.length()))); 100 | } 101 | }while (transactionRepository.existsByReferenceNum(builder.toString())); 102 | return builder.toString(); 103 | } 104 | 105 | /** 106 | * This method returns a list of transactions for a particular account by userId 107 | */ 108 | public List getTransactionHistoryByUserId(TransactionHistoryRequest request, int userId, Pageable pageable) { 109 | Account userAccount = accountService.getAccountByUserId(userId); 110 | Slice transactions = transactionRepository.findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 111 | TransactionStatus.SUCCESS, 112 | request.startDateTime(), 113 | request.endDateTime(), 114 | userAccount.getAccountNumber(), 115 | userAccount.getAccountNumber(), 116 | pageable 117 | ); 118 | if(transactions.getContent().isEmpty()){ 119 | throw new ResourceNotFoundException("no transactions"); 120 | } 121 | 122 | return formatTransactions(transactions.getContent(), userAccount.getAccountNumber()); 123 | 124 | } 125 | 126 | private List getTransactionHistoryByUserId(TransactionHistoryRequest request, int userId) { 127 | Account userAccount = accountService.getAccountByUserId(userId); 128 | List transactions = transactionRepository.findAllByStatusAndCreatedAtBetweenAndSenderAccountNumberOrReceiverAccountNumber( 129 | TransactionStatus.SUCCESS, 130 | request.startDateTime(), 131 | request.endDateTime(), 132 | userAccount.getAccountNumber(), 133 | userAccount.getAccountNumber() 134 | ); 135 | 136 | if(transactions.isEmpty()){ 137 | throw new ResourceNotFoundException("no transactions found"); 138 | } 139 | 140 | return transactions; 141 | 142 | } 143 | 144 | /** 145 | * 146 | * This method formats the transactions into the desired format which classifies each transaction into either credit and debit for easier understanding. 147 | */ 148 | private List formatTransactions(List transactions, String userAccountNumber){ 149 | 150 | List transactionHistoryResponses = new ArrayList<>(); 151 | 152 | transactions.forEach( 153 | transaction -> { 154 | TransactionHistoryResponse transactionHistoryResponse = new TransactionHistoryResponse(); 155 | transactionHistoryResponse.setTransactionDateTime(transaction.getCreatedAt()); 156 | transactionHistoryResponse.setAmount(transaction.getAmount()); 157 | transactionHistoryResponse.setReceiverName(transaction.getReceiverName()); 158 | transactionHistoryResponse.setSenderName(transaction.getSenderName()); 159 | transactionHistoryResponse.setTransactionType(checkTransactionType(transaction, userAccountNumber)); 160 | transactionHistoryResponses.add(transactionHistoryResponse); 161 | } 162 | ); 163 | 164 | return transactionHistoryResponses; 165 | } 166 | 167 | public TransactionType checkTransactionType(Transaction transaction, String userAccountNumber){ 168 | 169 | if(transaction.getReceiverAccountNumber().equals(userAccountNumber)){ 170 | return TransactionType.CREDIT; 171 | }else if(transaction.getSenderAccountNumber().equals(userAccountNumber)){ 172 | return TransactionType.DEBIT; 173 | } 174 | throw new IllegalArgumentException("error processing cannot determine transaction type"); 175 | } 176 | 177 | /** 178 | * This method generates an account statement for a particular account by userId, month, year and returns it as a pdf file 179 | */ 180 | public ByteArrayOutputStream generateTransactionStatement(TransactionHistoryRequest request, int userId) throws DocumentException { 181 | Account account = accountService.getAccountByUserId(userId); 182 | List transactions = getTransactionHistoryByUserId(request,userId); 183 | return formatTransactionHistoryToDocument(request, transactions, account); 184 | } 185 | 186 | 187 | private ByteArrayOutputStream formatTransactionHistoryToDocument(TransactionHistoryRequest request, List transactions, Account userAccount) throws DocumentException { 188 | 189 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 190 | Document document = new Document(); 191 | PdfWriter.getInstance(document, outputStream); 192 | document.open(); 193 | 194 | document.add(new Paragraph("Account Statement for " + request.startDateTime())); 195 | document.add(new Paragraph("Account Number: " + userAccount.getAccountNumber())); 196 | document.add(new Paragraph("Account Holder: " + userService.getUserByUserId(userAccount.getUserId()).getFullName())); 197 | document.add(Chunk.NEWLINE); 198 | 199 | Font boldFont = new Font(Font.FontFamily.TIMES_ROMAN, 12, Font.BOLD); 200 | PdfPTable table = new PdfPTable(new float[]{1, 1, 1, 1, 1, 1}); 201 | 202 | table.addCell(new PdfPCell(new Phrase("Reference Number", boldFont))); 203 | table.addCell(new PdfPCell(new Phrase("Transaction Date", boldFont))); 204 | table.addCell(new PdfPCell(new Phrase("Amount", boldFont))); 205 | table.addCell(new PdfPCell(new Phrase("Sender", boldFont))); 206 | table.addCell(new PdfPCell(new Phrase("Recipient", boldFont))); 207 | table.addCell(new PdfPCell(new Phrase("Description", boldFont))); 208 | 209 | transactions.forEach(transaction -> { 210 | table.addCell(new PdfPCell(new Phrase(transaction.getReferenceNum()))); 211 | table.addCell(new PdfPCell(new Phrase(transaction.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)))); 212 | table.addCell(new PdfPCell(new Phrase(String.format("%.2f", transaction.getAmount())))); 213 | table.addCell(new PdfPCell(new Phrase(transaction.getSenderName()))); 214 | table.addCell(new PdfPCell(new Phrase(transaction.getReceiverName()))); 215 | table.addCell(new PdfPCell(new Phrase(transaction.getDescription()))); 216 | }); 217 | 218 | document.add(table); 219 | 220 | document.close(); 221 | return outputStream; 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /src/test/java/com/amigoscode/group/ebankingsuite/account/AccountServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.amigoscode.group.ebankingsuite.account; 2 | 3 | import com.amigoscode.group.ebankingsuite.account.request.AccountTransactionPinUpdateModel; 4 | import com.amigoscode.group.ebankingsuite.account.response.AccountOverviewResponse; 5 | import com.amigoscode.group.ebankingsuite.exception.AccountNotActivatedException; 6 | import com.amigoscode.group.ebankingsuite.exception.AccountNotClearedException; 7 | import com.amigoscode.group.ebankingsuite.exception.InsufficientBalanceException; 8 | import com.amigoscode.group.ebankingsuite.exception.ResourceNotFoundException; 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.mockito.ArgumentCaptor; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import java.math.BigDecimal; 16 | import java.util.Optional; 17 | import static org.assertj.core.api.Java6Assertions.assertThat; 18 | import static org.assertj.core.api.Java6Assertions.assertThatThrownBy; 19 | import static org.mockito.BDDMockito.given; 20 | import static org.mockito.Mockito.verify; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class AccountServiceTest { 24 | 25 | @Mock 26 | private AccountRepository accountRepository; 27 | private AccountService accountService; 28 | 29 | 30 | @BeforeEach 31 | void setUp() { 32 | this.accountService = new AccountService(accountRepository); 33 | } 34 | 35 | @Test 36 | void canCreateAccount() { 37 | //given 38 | Account newAccount = new Account( 39 | 1, 40 | new BigDecimal(0), 41 | AccountStatus.ACTIVATED, 42 | "6767576476", 43 | Tier.LEVEL1, 44 | "8493" 45 | ); 46 | 47 | //when 48 | accountService.createAccount(newAccount); 49 | ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 50 | verify(accountRepository).save(accountArgumentCaptor.capture()); 51 | 52 | //then 53 | assertThat(accountArgumentCaptor.getValue()).isEqualToIgnoringGivenFields(newAccount,"id"); 54 | } 55 | 56 | @Test 57 | void canGetAccountByUserIdWhenAccountExists() { 58 | //given 59 | Integer userId = 1; 60 | Account mockUserAccount = new Account( 61 | 1, 62 | new BigDecimal(0), 63 | AccountStatus.ACTIVATED, 64 | "6767576476", 65 | Tier.LEVEL1, 66 | "8493" 67 | ); 68 | 69 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(mockUserAccount)); 70 | 71 | //when 72 | Account userAccount = accountService.getAccountByUserId(userId); 73 | //then 74 | assertThat(userAccount).isEqualToIgnoringGivenFields(mockUserAccount,"id"); 75 | } 76 | @Test 77 | void willThrowResourceNotFoundExceptionWhenNoAccountFoundForUserId() { 78 | //given 79 | Integer userId = 1; 80 | 81 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.empty()); 82 | 83 | //when 84 | //then 85 | assertThatThrownBy(() -> accountService.getAccountByUserId(userId)) 86 | .isInstanceOf(ResourceNotFoundException.class) 87 | .hasMessage("account not found"); 88 | } 89 | 90 | @Test 91 | void canGenerateAccountOverviewByUserId() { 92 | //given 93 | Integer userId = 1; 94 | Account newAccount = new Account( 95 | 1, 96 | new BigDecimal(0), 97 | AccountStatus.ACTIVATED, 98 | "6767576476", 99 | Tier.LEVEL1, 100 | "8493" 101 | ); 102 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(newAccount)); 103 | 104 | //when 105 | AccountOverviewResponse response = accountService.generateAccountOverviewByUserId(userId); 106 | //then 107 | assertThat(response).isEqualTo(new AccountOverviewResponse( 108 | newAccount.getAccountBalance(), 109 | newAccount.getAccountNumber(), 110 | newAccount.getTierLevel().name(), 111 | newAccount.getAccountStatus().name()) 112 | ); 113 | 114 | } 115 | 116 | @Test 117 | void canCloseAccountWhenAccountIsCleared(){ 118 | //given 119 | int userId = 1; 120 | Account userAccount = new Account( 121 | 1, 122 | new BigDecimal(0), 123 | AccountStatus.ACTIVATED, 124 | "6767576476", 125 | Tier.LEVEL1, 126 | "8493" 127 | ); 128 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(userAccount)); 129 | 130 | //when 131 | accountService.closeAccount(userId); 132 | ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 133 | verify(accountRepository).save(accountArgumentCaptor.capture()); 134 | 135 | //then 136 | assertThat(accountArgumentCaptor.getValue().getAccountStatus()).isEqualTo(AccountStatus.CLOSED); 137 | 138 | } 139 | 140 | @Test 141 | void willThrowAccountNotClearedExceptionWhenClosingAccountThatIsNotCleared(){ 142 | //given 143 | int userId = 1; 144 | Account userAccount = new Account( 145 | 1, 146 | new BigDecimal(10), 147 | AccountStatus.ACTIVATED, 148 | "6767576476", 149 | Tier.LEVEL1, 150 | "8493" 151 | ); 152 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(userAccount)); 153 | 154 | //when 155 | //then 156 | assertThatThrownBy(() -> accountService.closeAccount(userId)) 157 | .isInstanceOf(AccountNotClearedException.class); 158 | 159 | } 160 | 161 | @Test 162 | void canUpdateTransactionPinIfPinConformsToStandard(){ 163 | //given 164 | AccountTransactionPinUpdateModel pinUpdateModel = 165 | new AccountTransactionPinUpdateModel("1234"); 166 | int userId = 1; 167 | Account userAccount = new Account( 168 | 1, 169 | new BigDecimal(10), 170 | AccountStatus.ACTIVATED, 171 | "6767576476", 172 | Tier.LEVEL1, 173 | "8493" 174 | ); 175 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(userAccount)); 176 | 177 | //when 178 | accountService.updateAccountTransactionPin(userId,pinUpdateModel); 179 | ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 180 | verify(accountRepository).save(accountArgumentCaptor.capture()); 181 | 182 | //then 183 | assertThat(accountArgumentCaptor.getValue()) 184 | .isEqualToComparingOnlyGivenFields(userAccount, 185 | "id","accountBalance" 186 | ,"accountStatus","accountNumber","tierLevel"); 187 | } 188 | @Test 189 | void willThrowIllegalArgumentExceptionIfPinDoesNotConformToStandardWhenUpdatingTransactionPin(){ 190 | //given 191 | AccountTransactionPinUpdateModel pinUpdateModel = 192 | new AccountTransactionPinUpdateModel("12344"); 193 | int userId = 1; 194 | Account userAccount = new Account( 195 | 1, 196 | new BigDecimal(10), 197 | AccountStatus.ACTIVATED, 198 | "6767576476", 199 | Tier.LEVEL1, 200 | "8433" 201 | ); 202 | given(accountRepository.findAccountByUserId(userId)).willReturn(Optional.of(userAccount)); 203 | 204 | //when 205 | //then 206 | assertThatThrownBy(() -> accountService.updateAccountTransactionPin(userId,pinUpdateModel)) 207 | .isInstanceOf(IllegalArgumentException.class); 208 | } 209 | 210 | @Test 211 | void canCreditAccount(){ 212 | //given 213 | BigDecimal amount = new BigDecimal("300"); 214 | Account receiverAccount = new Account( 215 | 1, 216 | new BigDecimal(10), 217 | AccountStatus.ACTIVATED, 218 | "6767576476", 219 | Tier.LEVEL1, 220 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa" 221 | ); 222 | //when 223 | accountService.creditAccount(receiverAccount,amount); 224 | ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 225 | verify(accountRepository).save(accountArgumentCaptor.capture()); 226 | 227 | //then 228 | assertThat(accountArgumentCaptor.getValue().getAccountBalance(). 229 | compareTo(receiverAccount.getAccountBalance().add(amount))==0); 230 | assertThat(accountArgumentCaptor.getValue()) 231 | .isEqualToComparingOnlyGivenFields(receiverAccount, 232 | "id","accountStatus","accountNumber","tierLevel"); 233 | } 234 | 235 | @Test 236 | void canDebitAccount(){ 237 | //given 238 | BigDecimal amount = new BigDecimal("300"); 239 | Account senderAccount = new Account( 240 | 1, 241 | new BigDecimal(5000), 242 | AccountStatus.ACTIVATED, 243 | "6767576476", 244 | Tier.LEVEL1, 245 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa" 246 | ); 247 | //when 248 | accountService.debitAccount(senderAccount,amount); 249 | ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 250 | verify(accountRepository).save(accountArgumentCaptor.capture()); 251 | 252 | //then 253 | assertThat(accountArgumentCaptor.getValue().getAccountBalance(). 254 | compareTo(senderAccount.getAccountBalance().subtract(amount))==0); 255 | assertThat(accountArgumentCaptor.getValue()) 256 | .isEqualToComparingOnlyGivenFields(senderAccount, 257 | "id","accountStatus","accountNumber","tierLevel"); 258 | } 259 | 260 | @Test 261 | void willThrowInsufficientFundsIfSenderDoesNotHaveEnoughFunds(){ 262 | //given 263 | BigDecimal amount = new BigDecimal("300"); 264 | Account senderAccount = new Account( 265 | 1, 266 | new BigDecimal(200), 267 | AccountStatus.ACTIVATED, 268 | "6767576476", 269 | Tier.LEVEL1, 270 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa" 271 | ); 272 | //when 273 | //then 274 | assertThatThrownBy(()-> accountService.debitAccount(senderAccount,amount)) 275 | .isInstanceOf(InsufficientBalanceException.class) 276 | .hasMessage("Insufficient funds"); 277 | } 278 | 279 | @Test 280 | void canReturnAccountWhenAccountExistsAndIsValid(){ 281 | //given 282 | 283 | String accountNumber = "6767576476"; 284 | Account account = new Account( 285 | 1, 286 | new BigDecimal(5000), 287 | AccountStatus.ACTIVATED, 288 | "6767576476", 289 | Tier.LEVEL1, 290 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa" 291 | ); 292 | given(accountRepository.findAccountByAccountNumber(accountNumber)).willReturn(Optional.of(account)); 293 | 294 | //when 295 | Account existingAccount = accountService.accountExistsAndIsActivated(accountNumber); 296 | 297 | //then 298 | 299 | assertThat(existingAccount).isEqualToComparingOnlyGivenFields( 300 | account, 301 | "userId","accountBalance","accountNumber","tierLevel"); 302 | } 303 | 304 | @Test 305 | void willThrowAccountNotActivateExceptionWhenAccountStatusIsNotActivated(){ 306 | 307 | //given 308 | String accountNumber = "6767576476"; 309 | Account account = new Account( 310 | 1, 311 | new BigDecimal(5000), 312 | AccountStatus.BLOCKED, 313 | "6767576476", 314 | Tier.LEVEL1, 315 | "$2a$10$j4ogRjGJWnPUrmdE82Mq5ueybC9SxGTCgQkvzzE7uSbYXoKqIMKxa" 316 | ); 317 | given(accountRepository.findAccountByAccountNumber(accountNumber)).willReturn(Optional.of(account)); 318 | 319 | //when 320 | //then 321 | assertThatThrownBy(()->accountService.accountExistsAndIsActivated(accountNumber)) 322 | .isInstanceOf(AccountNotActivatedException.class) 323 | .hasMessage("Account not activated"); 324 | } 325 | 326 | @Test 327 | void willThrowResourceNotFoundExceptionWhenAccountDoesNotExist(){ 328 | //given 329 | String accountNumber = "6767576476"; 330 | given(accountRepository.findAccountByAccountNumber(accountNumber)).willReturn(Optional.empty()); 331 | 332 | //when 333 | //then 334 | assertThatThrownBy(()->accountService.accountExistsAndIsActivated(accountNumber)) 335 | .isInstanceOf(ResourceNotFoundException.class) 336 | .hasMessage("Account not found"); 337 | } 338 | 339 | 340 | // @Test 341 | // void updateAccount() { 342 | // //given 343 | // Account existingAccount = new Account( 344 | // 1, 345 | // new BigDecimal(0), 346 | // AccountStatus.ACTIVATED, 347 | // "6767576476", 348 | // Tier.LEVEL1, 349 | // "8493" 350 | // ); 351 | // 352 | // //when 353 | // accountService.createAccount(newAccount); 354 | // ArgumentCaptor accountArgumentCaptor = ArgumentCaptor.forClass(Account.class); 355 | // verify(accountRepository).save(accountArgumentCaptor.capture()); 356 | // 357 | // //then 358 | // assertThat(accountArgumentCaptor.getValue()).isEqualToIgnoringGivenFields(newAccount,"id"); 359 | // } 360 | } --------------------------------------------------------------------------------