Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import com.example.bankapp.model.Transaction;
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;

public interface TransactionRepository extends JpaRepository<Transaction, Long> {
List<Transaction> findByAccountId(Long accountId);

List<Transaction> findByAccountIdAndTimestampBetween(Long accountId, LocalDateTime start, LocalDateTime end);
}
25 changes: 24 additions & 1 deletion src/main/java/com/example/bankapp/service/AccountService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.bankapp.repository.AccountRepository;
import com.example.bankapp.repository.TransactionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
Expand All @@ -14,6 +15,7 @@
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -31,6 +33,9 @@ public class AccountService implements UserDetailsService {
@Autowired
private TransactionRepository transactionRepository;

@Value("${bankapp.daily-withdrawal-limit}")
private BigDecimal dailyWithdrawalLimit;

public Account findAccountByUsername(String username) {
return accountRepository.findByUsername(username).orElseThrow(() -> new RuntimeException("Account not found"));
}
Expand Down Expand Up @@ -62,6 +67,13 @@ public void deposit(Account account, BigDecimal amount) {
}

public void withdraw(Account account, BigDecimal amount) {
if (amount == null || amount.signum() <= 0) {
throw new RuntimeException("Withdrawal amount must be positive");
}
LocalDateTime now = LocalDateTime.now();
if (getWithdrawnOn(account, now.toLocalDate()).add(amount).compareTo(dailyWithdrawalLimit) > 0) {
throw new RuntimeException("Daily withdrawal limit of $" + dailyWithdrawalLimit.toPlainString() + " exceeded");
}
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("Insufficient funds");
}
Expand All @@ -71,12 +83,23 @@ public void withdraw(Account account, BigDecimal amount) {
Transaction transaction = new Transaction(
amount,
"Withdrawal",
LocalDateTime.now(),
now,
account
);
transactionRepository.save(transaction);
}

private BigDecimal getWithdrawnOn(Account account, LocalDate day) {
LocalDateTime startOfDay = day.atStartOfDay();
LocalDateTime startOfNextDay = startOfDay.plusDays(1);
return transactionRepository
.findByAccountIdAndTimestampBetween(account.getId(), startOfDay, startOfNextDay)
.stream()
.filter(transaction -> "Withdrawal".equals(transaction.getType()))
.map(Transaction::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}

public List<Transaction> getTransactionHistory(Account account) {
return transactionRepository.findByAccountId(account.getId());
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.show-sql=true

# Banking rules
bankapp.daily-withdrawal-limit=5000
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.example.bankapp.service;

import com.example.bankapp.model.Account;
import com.example.bankapp.model.Transaction;
import com.example.bankapp.repository.AccountRepository;
import com.example.bankapp.repository.TransactionRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AccountServiceDailyWithdrawalLimitTest {

@Mock
private AccountRepository accountRepository;

@Mock
private TransactionRepository transactionRepository;

@InjectMocks
private AccountService accountService;

private Account account;

@BeforeEach
void setUp() {
ReflectionTestUtils.setField(accountService, "dailyWithdrawalLimit", new BigDecimal("5000"));
account = new Account();
account.setId(1L);
account.setBalance(new BigDecimal("10000"));
}

private void givenTodaysTransactions(Transaction... transactions) {
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
when(transactionRepository.findByAccountIdAndTimestampBetween(1L, startOfDay, startOfDay.plusDays(1)))
.thenReturn(List.of(transactions));
}

private Transaction today(String amount, String type) {
return new Transaction(new BigDecimal(amount), type, LocalDateTime.now(), account);
}

@Test
void allowsWithdrawalsUpToTheDailyLimit() {
givenTodaysTransactions(today("3000", "Withdrawal"));

accountService.withdraw(account, new BigDecimal("2000"));

assertEquals(new BigDecimal("8000"), account.getBalance());
verify(accountRepository).save(account);
verify(transactionRepository).save(any(Transaction.class));
}

@Test
void rejectsWithdrawalThatPushesTodaysTotalOverTheLimit() {
givenTodaysTransactions(today("3000", "Withdrawal"), today("1500", "Withdrawal"));

RuntimeException ex = assertThrows(RuntimeException.class,
() -> accountService.withdraw(account, new BigDecimal("500.01")));

assertEquals("Daily withdrawal limit of $5000 exceeded", ex.getMessage());
assertEquals(new BigDecimal("10000"), account.getBalance());
verify(accountRepository, never()).save(any());
verify(transactionRepository, never()).save(any());
}

@Test
void rejectsSingleWithdrawalOverTheLimit() {
givenTodaysTransactions();

RuntimeException ex = assertThrows(RuntimeException.class,
() -> accountService.withdraw(account, new BigDecimal("5001")));

assertEquals("Daily withdrawal limit of $5000 exceeded", ex.getMessage());
}

@Test
void ignoresDepositsAndTransfersWhenSummingTodaysWithdrawals() {
givenTodaysTransactions(
today("4000", "Deposit"),
today("4000", "Transfer Out to bob"),
today("1000", "Withdrawal"));

accountService.withdraw(account, new BigDecimal("4000"));

assertEquals(new BigDecimal("6000"), account.getBalance());
}

@Test
void keepsInsufficientFundsCheckWithinTheLimit() {
account.setBalance(new BigDecimal("100"));
givenTodaysTransactions();

RuntimeException ex = assertThrows(RuntimeException.class,
() -> accountService.withdraw(account, new BigDecimal("200")));

assertEquals("Insufficient funds", ex.getMessage());
verify(transactionRepository, never()).save(any());
verify(transactionRepository).findByAccountIdAndTimestampBetween(eq(1L), any(), any());
}

@Test
void rejectsNonPositiveAmountsSoTheyCannotRestoreDailyAllowance() {
RuntimeException ex = assertThrows(RuntimeException.class,
() -> accountService.withdraw(account, new BigDecimal("-1000")));

assertEquals("Withdrawal amount must be positive", ex.getMessage());
assertThrows(RuntimeException.class, () -> accountService.withdraw(account, BigDecimal.ZERO));
assertEquals(new BigDecimal("10000"), account.getBalance());
verify(accountRepository, never()).save(any());
verify(transactionRepository, never()).save(any());
}
}
Loading