From 1f69e38232bec0c7630fa125f7ddbe1c8d7d0d6b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:59:37 +0000 Subject: [PATCH 1/2] feature: enforce configurable daily withdrawal limit Co-Authored-By: Grace Juan --- .../repository/TransactionRepository.java | 3 + .../bankapp/service/AccountService.java | 19 +++ src/main/resources/application.properties | 3 + ...ccountServiceDailyWithdrawalLimitTest.java | 118 ++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java diff --git a/src/main/java/com/example/bankapp/repository/TransactionRepository.java b/src/main/java/com/example/bankapp/repository/TransactionRepository.java index 7d4f2578..fab472ec 100644 --- a/src/main/java/com/example/bankapp/repository/TransactionRepository.java +++ b/src/main/java/com/example/bankapp/repository/TransactionRepository.java @@ -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 { List findByAccountId(Long accountId); + + List findByAccountIdAndTimestampBetween(Long accountId, LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/com/example/bankapp/service/AccountService.java b/src/main/java/com/example/bankapp/service/AccountService.java index 5d7d90ec..cd89614d 100644 --- a/src/main/java/com/example/bankapp/service/AccountService.java +++ b/src/main/java/com/example/bankapp/service/AccountService.java @@ -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; @@ -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; @@ -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")); } @@ -62,6 +67,9 @@ public void deposit(Account account, BigDecimal amount) { } public void withdraw(Account account, BigDecimal amount) { + if (getWithdrawnToday(account).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"); } @@ -77,6 +85,17 @@ public void withdraw(Account account, BigDecimal amount) { transactionRepository.save(transaction); } + private BigDecimal getWithdrawnToday(Account account) { + LocalDateTime startOfDay = LocalDate.now().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 getTransactionHistory(Account account) { return transactionRepository.findByAccountId(account.getId()); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 08663a63..df971e0c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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 diff --git a/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java b/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java new file mode 100644 index 00000000..746f036a --- /dev/null +++ b/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java @@ -0,0 +1,118 @@ +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()); + } +} From abd2c632177f1f99c0468762b21802ec7c1a1215 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 22:02:05 +0000 Subject: [PATCH 2/2] feature: reject non-positive withdrawals and use one clock read per withdrawal Co-Authored-By: Grace Juan --- .../com/example/bankapp/service/AccountService.java | 12 ++++++++---- .../AccountServiceDailyWithdrawalLimitTest.java | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/bankapp/service/AccountService.java b/src/main/java/com/example/bankapp/service/AccountService.java index cd89614d..fa844c33 100644 --- a/src/main/java/com/example/bankapp/service/AccountService.java +++ b/src/main/java/com/example/bankapp/service/AccountService.java @@ -67,7 +67,11 @@ public void deposit(Account account, BigDecimal amount) { } public void withdraw(Account account, BigDecimal amount) { - if (getWithdrawnToday(account).add(amount).compareTo(dailyWithdrawalLimit) > 0) { + 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) { @@ -79,14 +83,14 @@ public void withdraw(Account account, BigDecimal amount) { Transaction transaction = new Transaction( amount, "Withdrawal", - LocalDateTime.now(), + now, account ); transactionRepository.save(transaction); } - private BigDecimal getWithdrawnToday(Account account) { - LocalDateTime startOfDay = LocalDate.now().atStartOfDay(); + private BigDecimal getWithdrawnOn(Account account, LocalDate day) { + LocalDateTime startOfDay = day.atStartOfDay(); LocalDateTime startOfNextDay = startOfDay.plusDays(1); return transactionRepository .findByAccountIdAndTimestampBetween(account.getId(), startOfDay, startOfNextDay) diff --git a/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java b/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java index 746f036a..5d44d6b5 100644 --- a/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java +++ b/src/test/java/com/example/bankapp/service/AccountServiceDailyWithdrawalLimitTest.java @@ -115,4 +115,16 @@ void keepsInsufficientFundsCheckWithinTheLimit() { 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()); + } }