diff --git a/src/main/java/honours/ing/banq/account/BankAccountServiceImpl.java b/src/main/java/honours/ing/banq/account/BankAccountServiceImpl.java index ece0db8..011fdfb 100644 --- a/src/main/java/honours/ing/banq/account/BankAccountServiceImpl.java +++ b/src/main/java/honours/ing/banq/account/BankAccountServiceImpl.java @@ -101,12 +101,12 @@ public Object closeAccount(String authToken, String iBAN) throws NotAuthorizedEr } Account savingsAccount = account.getSavingAccount(); - if (savingsAccount != null && !savingsAccount.getBalance().equals(BigDecimal.ZERO)) { + if (savingsAccount != null && savingsAccount.getBalance().compareTo(BigDecimal.ZERO) != 0) { throw new InvalidParamValueError("Account balance needs to be cleared"); } Account checkingAccount = account.getCheckingAccount(); - if (checkingAccount != null && !checkingAccount.getBalance().equals(BigDecimal.ZERO)) { + if (checkingAccount != null && checkingAccount.getBalance().compareTo(BigDecimal.ZERO) != 0) { throw new InvalidParamValueError("Account balance needs to be cleared"); } diff --git a/src/main/java/honours/ing/banq/event/InterestEvent.java b/src/main/java/honours/ing/banq/event/InterestEvent.java new file mode 100644 index 0000000..3eb47cb --- /dev/null +++ b/src/main/java/honours/ing/banq/event/InterestEvent.java @@ -0,0 +1,29 @@ +package honours.ing.banq.event; + +import java.util.Calendar; +import java.util.Date; + +import static java.util.Calendar.*; +import static java.util.Calendar.DAY_OF_MONTH; + +/** + * @author jeffrey + * @since 12-8-17 + */ +public abstract class InterestEvent implements Event { + + @Override + public long nextIteration(long lastIteration) { + Calendar c = Calendar.getInstance(); + c.setTime(new Date(lastIteration)); + + c.set(HOUR_OF_DAY, 0); + c.set(MINUTE, 0); + c.set(SECOND, 0); + c.set(MILLISECOND, 0); + + c.add(DAY_OF_MONTH, 1); + return c.getTimeInMillis(); + } + +} diff --git a/src/main/java/honours/ing/banq/event/OverdraftInterestEvent.java b/src/main/java/honours/ing/banq/event/OverdraftInterestEvent.java index 649a296..3b203c5 100644 --- a/src/main/java/honours/ing/banq/event/OverdraftInterestEvent.java +++ b/src/main/java/honours/ing/banq/event/OverdraftInterestEvent.java @@ -2,6 +2,7 @@ import honours.ing.banq.account.BankAccount; import honours.ing.banq.account.BankAccountRepository; +import honours.ing.banq.account.CheckingAccount; import honours.ing.banq.transaction.TransactionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -19,7 +20,7 @@ * @since 5-8-17 */ @Component -public class OverdraftInterestEvent implements Event { +public class OverdraftInterestEvent extends InterestEvent { private static final BigDecimal INTEREST = new BigDecimal("0.10"); private static final BigDecimal MONTHLY_RATE = new BigDecimal(Math.pow(INTEREST.doubleValue() + 1.0, 1.0 / 12.0)) @@ -59,33 +60,21 @@ public void execute(long time) { continue; } - account.getCheckingAccount().addInterest(account.getCheckingAccount().getLowestBalance().multiply(interest).multiply(new BigDecimal(-1.0))); - account.getCheckingAccount().resetLowestBalance(); + CheckingAccount ca = account.getCheckingAccount(); + + ca.addInterest(ca.getLowestBalance().multiply(interest).multiply(new BigDecimal(-1.0))); + ca.resetLowestBalance(); if (firstOfMonth) { transactionService.forceTransactionAccount( - account.getCheckingAccount(), account.getCheckingAccount().getInterest() + ca, ca.getInterest() .multiply(new BigDecimal("-1.0")) .setScale(2, BigDecimal.ROUND_HALF_UP), "Interest"); - account.getCheckingAccount().resetInterest(); + ca.resetInterest(); } accountRepository.save(account); } } - @Override - public long nextIteration(long lastIteration) { - Calendar c = Calendar.getInstance(); - c.setTime(new Date(lastIteration)); - - c.set(HOUR_OF_DAY, 0); - c.set(MINUTE, 0); - c.set(SECOND, 0); - c.set(MILLISECOND, 0); - - c.add(DAY_OF_MONTH, 1); - return c.getTimeInMillis(); - } - } diff --git a/src/main/java/honours/ing/banq/event/SavingsInterestEvent.java b/src/main/java/honours/ing/banq/event/SavingsInterestEvent.java new file mode 100644 index 0000000..93ec93b --- /dev/null +++ b/src/main/java/honours/ing/banq/event/SavingsInterestEvent.java @@ -0,0 +1,117 @@ +package honours.ing.banq.event; + +import honours.ing.banq.account.BankAccount; +import honours.ing.banq.account.BankAccountRepository; +import honours.ing.banq.account.SavingAccount; +import honours.ing.banq.transaction.TransactionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import static java.util.Calendar.*; + +/** + * @author Jeffrey Bakker + * @since 5-8-17 + */ +@Component +public class SavingsInterestEvent extends InterestEvent { + + private static final InterestRate[] RATES = { + new InterestRate(new BigDecimal("0.0015"), new BigDecimal("25000")), + new InterestRate(new BigDecimal("0.0015"), new BigDecimal("75000")), + new InterestRate(new BigDecimal("0.0020"), new BigDecimal("1000000")), + }; + + // Services + private TransactionService transactionService; + + // Repositories + private BankAccountRepository accountRepository; + + @Autowired + public SavingsInterestEvent(TransactionService transactionService, BankAccountRepository accountRepository) { + this.transactionService = transactionService; + this.accountRepository = accountRepository; + } + + @Override + public void execute(long time) { + final Calendar c = Calendar.getInstance(); + c.setTime(new Date(time)); + + //noinspection MagicConstant + final boolean firstOfYear = c.get(DAY_OF_MONTH) == 1 && c.get(MONTH) == 0; + final InterestRate[] rates = calcDailyRates(c, RATES); + + List accounts = accountRepository.findAll(); + + for (BankAccount account : accounts) { + SavingAccount sa = account.getSavingAccount(); + if (sa == null) { + continue; + } + + BigDecimal balance = sa.getLowestBalance(); + BigDecimal lastMax = new BigDecimal("0.00"); + BigDecimal interest = new BigDecimal("0.00"); + + for (InterestRate rate : rates) { + balance = balance.subtract(lastMax); + if (balance.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + + BigDecimal delta = rate.maxBalance.subtract(lastMax); + lastMax = rate.maxBalance; + + BigDecimal rateAmt = balance.min(delta); + interest = interest.add(rateAmt.multiply(rate.interest)); + } + + sa.addInterest(interest); + sa.resetLowestBalance(); + + if (firstOfYear) { + transactionService.forceTransactionAccount( + sa, sa.getInterest() + .setScale(2, BigDecimal.ROUND_HALF_UP), "Interest"); + sa.resetInterest(); + } + + accountRepository.save(account); + } + } + + private static InterestRate[] calcDailyRates(Calendar now, InterestRate[] yearlyRates) { + InterestRate[] res = new InterestRate[yearlyRates.length]; + + final int daysInYear = now.getActualMaximum(DAY_OF_YEAR); + + for (int i = 0; i < res.length; i++) { + res[i] = new InterestRate( + yearlyRates[i].interest.divide(new BigDecimal((double) daysInYear), 7, RoundingMode.HALF_UP), + yearlyRates[i].maxBalance + ); + } + + return res; + } + + private static class InterestRate { + + public BigDecimal interest; + public BigDecimal maxBalance; + + public InterestRate(BigDecimal interest, BigDecimal maxBalance) { + this.interest = interest; + this.maxBalance = maxBalance; + } + } + +} diff --git a/src/main/java/honours/ing/banq/info/bean/BalanceBean.java b/src/main/java/honours/ing/banq/info/bean/BalanceBean.java index 6917cf4..3fc2af7 100644 --- a/src/main/java/honours/ing/banq/info/bean/BalanceBean.java +++ b/src/main/java/honours/ing/banq/info/bean/BalanceBean.java @@ -10,12 +10,19 @@ public class BalanceBean { private Double balance; + private Double savingsAccountBalance; public BalanceBean(BankAccount bankAccount) { balance = bankAccount.getCheckingAccount().getBalance().doubleValue(); + savingsAccountBalance = bankAccount.getSavingAccount() == null + ? 0.0d : bankAccount.getSavingAccount().getBalance().doubleValue(); } public Double getBalance() { return balance; } + + public Double getSavingsAccountBalance() { + return savingsAccountBalance; + } } diff --git a/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java b/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java index 1642d6a..a664543 100644 --- a/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java +++ b/src/main/java/honours/ing/banq/transaction/TransactionServiceImpl.java @@ -58,10 +58,14 @@ public void depositIntoAccount(String iBAN, String pinCard, String pinCode, Doub throw new InvalidParamValueError("Amount should be greater than 0."); } - CheckingAccount account = bankAccount.getCheckingAccount(); + Account account = iBAN.endsWith("S") + ? bankAccount.getSavingAccount() : bankAccount.getCheckingAccount(); + if (account == null) { + throw new InvalidParamValueError("The account does not exist"); + } // Update balance - account.addBalance(new BigDecimal(amount)); + account.addBalance(new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP)); bankAccountRepository.save(bankAccount); // Save transaction @@ -87,10 +91,17 @@ public void payFromAccount(String sourceIBAN, String targetIBAN, String pinCard, sourceBankAccount.getSavingAccount() : sourceBankAccount.getCheckingAccount(); BankAccount destBankAccount = bankAccountRepository.findOne((int) IBANUtil.getAccountNumber(targetIBAN)); - Account destAccount = targetIBAN.endsWith("S") ? - destBankAccount.getSavingAccount() : destBankAccount.getCheckingAccount(); + Account destAccount = null; + String destName = ""; + + if (destBankAccount != null) { + destAccount = targetIBAN.endsWith("S") ? + destBankAccount.getSavingAccount() : destBankAccount.getCheckingAccount(); + destName = destBankAccount.getPrimaryHolder().getName() + " " + + destBankAccount.getPrimaryHolder().getSurname(); + } - BigDecimal amt = new BigDecimal(amount); + BigDecimal amt = new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP); // Check balance if (amount <= 0.0d) { @@ -103,20 +114,23 @@ public void payFromAccount(String sourceIBAN, String targetIBAN, String pinCard, // Update balance sourceAccount.addBalance(amt.multiply(new BigDecimal(-1.0))); - destAccount.addBalance(amt); bankAccountRepository.save(sourceBankAccount); - bankAccountRepository.save(destBankAccount); + + if (destBankAccount != null) { + destAccount.addBalance(amt); + bankAccountRepository.save(destBankAccount); + } // Save Transaction Transaction transaction = new Transaction( - sourceIBAN, targetIBAN, destBankAccount.getPrimaryHolder().getName(), + sourceIBAN, targetIBAN, destName, timeService.getDate().getDate(), amount, "Payment with debit card."); transactionRepository.save(transaction); } @Override public void forceTransactionAccount(Account account, BigDecimal amount, String description) { - account.addBalance(amount); + account.addBalance(amount.setScale(2, BigDecimal.ROUND_HALF_UP)); bankAccountRepository.save(account.getAccount()); String iBAN = IBANUtil.generateIBAN(account.getAccount()) + (account instanceof CheckingAccount ? "S" : ""); @@ -154,7 +168,7 @@ public void transferMoney(String authToken, String sourceIBAN, String targetIBAN BankAccount destBankAccount = bankAccountRepository.findOne((int) IBANUtil.getAccountNumber(targetIBAN)); Account destAccount = destBankAccount.getCheckingAccount(); - BigDecimal amt = new BigDecimal(amount); + BigDecimal amt = new BigDecimal(amount).setScale(2, BigDecimal.ROUND_HALF_UP); // Check balance if (amount <= 0.0d) { diff --git a/src/test/java/honours/ing/banq/BoilerplateTest.java b/src/test/java/honours/ing/banq/BoilerplateTest.java index 4dd5243..833dbd9 100644 --- a/src/test/java/honours/ing/banq/BoilerplateTest.java +++ b/src/test/java/honours/ing/banq/BoilerplateTest.java @@ -1,12 +1,16 @@ package honours.ing.banq; +import honours.ing.banq.access.NoEffectError; import honours.ing.banq.account.BankAccountService; import honours.ing.banq.auth.AuthService; import honours.ing.banq.bean.AccountInfo; +import honours.ing.banq.card.CardService; import honours.ing.banq.config.TestConfiguration; import honours.ing.banq.info.InfoService; +import honours.ing.banq.info.bean.BalanceBean; import honours.ing.banq.time.TimeService; import honours.ing.banq.transaction.TransactionService; +import honours.ing.banq.util.IBANUtil; import org.junit.After; import org.junit.Before; import org.junit.Ignore; @@ -44,6 +48,9 @@ public class BoilerplateTest { @Autowired protected AuthService authService; + @Autowired + protected CardService cardService; + @Autowired protected InfoService infoService; @@ -86,6 +93,51 @@ public void tearDown() throws Exception { account1.token = authService.getAuthToken("jantje96", "1234").getAuthToken(); account2.token = authService.getAuthToken("piet1", "1234").getAuthToken(); + // Unblock pin cards + try { + cardService.unblockCard(account1.token, account1.iBan, account1.cardNumber); + } catch (NoEffectError ignored) { } + + try { + cardService.unblockCard(account2.token, account2.iBan, account2.cardNumber); + } catch (NoEffectError ignored) { } + + // Clear balance from account 1 + BalanceBean acc1 = infoService.getBalance(account1.token, account1.iBan); + if (acc1.getBalance() <= -0.01d) { + transactionService.depositIntoAccount( + account1.iBan, account1.cardNumber, account1.pin, acc1.getBalance() * -1.0d); + } else if (acc1.getBalance() >= 0.01d) { + transactionService.payFromAccount(account1.iBan, IBANUtil.generateIBAN(12345678), + account1.cardNumber, account1.pin, acc1.getBalance()); + } + + if (acc1.getSavingsAccountBalance() <= -0.01d) { + transactionService.depositIntoAccount( + account1.iBan + "S", account1.cardNumber, account1.pin, acc1.getSavingsAccountBalance() * -1.0d); + } else if (acc1.getSavingsAccountBalance() >= 0.01d) { + transactionService.payFromAccount(account1.iBan + "S", IBANUtil.generateIBAN(12345678), + account1.cardNumber, account1.pin, acc1.getSavingsAccountBalance()); + } + + // Clear balance from account 2 + BalanceBean acc2 = infoService.getBalance(account2.token, account2.iBan); + if (acc2.getBalance() <= -0.01d) { + transactionService.depositIntoAccount( + account2.iBan, account2.cardNumber, account2.pin, acc2.getBalance() * -1.0d); + } else if (acc2.getBalance() >= 0.01d) { + transactionService.payFromAccount(account2.iBan, IBANUtil.generateIBAN(12345678), + account2.cardNumber, account2.pin, acc2.getBalance()); + } + + if (acc2.getSavingsAccountBalance() <= -0.01d) { + transactionService.depositIntoAccount( + account2.iBan + "S", account2.cardNumber, account2.pin, acc2.getSavingsAccountBalance() * -1.0d); + } else if (acc2.getSavingsAccountBalance() >= 0.01d) { + transactionService.payFromAccount(account2.iBan + "S", IBANUtil.generateIBAN(12345678), + account2.cardNumber, account2.pin, acc2.getSavingsAccountBalance()); + } + accountService.closeAccount(account1.token, account1.iBan); accountService.closeAccount(account2.token, account2.iBan); @@ -97,4 +149,9 @@ protected void testBalance(AccountInfo account, double expected, double accuracy assertEquals(expected, infoService.getBalance(account.token, account.iBan).getBalance(), accuracy); } + protected void testSavingsBalance(AccountInfo account, double expected, double accuracy) throws Exception { + account.token = authService.getAuthToken(account.username, account.password).getAuthToken(); + assertEquals(expected, infoService.getBalance(account.token, account.iBan).getSavingsAccountBalance(), accuracy); + } + } diff --git a/src/test/java/honours/ing/banq/event/SavingsInterestEventTest.java b/src/test/java/honours/ing/banq/event/SavingsInterestEventTest.java new file mode 100644 index 0000000..011fa33 --- /dev/null +++ b/src/test/java/honours/ing/banq/event/SavingsInterestEventTest.java @@ -0,0 +1,49 @@ +package honours.ing.banq.event; + +import honours.ing.banq.BoilerplateTest; +import honours.ing.banq.time.TimeServiceImpl; +import org.junit.Test; + +import java.util.Calendar; +import java.util.Date; + +import static org.junit.Assert.*; + +/** + * @author jeffrey + * @since 12-8-17 + */ +public class SavingsInterestEventTest extends BoilerplateTest { + + @Test + public void testInterest() throws Exception { + long now = TimeServiceImpl.currentTimeMillis(); + + Calendar c = Calendar.getInstance(); + c.setTime(new Date(now)); + c.add(Calendar.YEAR, 1); + c.set(Calendar.MONTH, Calendar.JANUARY); + c.set(Calendar.DAY_OF_MONTH, 1); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + + long nextJan = c.getTimeInMillis(); + long diff = (nextJan - now) / (1000 * 60 * 60 * 24) + 1; + + // Shift the time to the first of january so that all the math is correct + timeService.simulateTime((int) diff); + + account1.token = authService.getAuthToken(account1.username, account1.password).getAuthToken(); + accountService.openSavingsAccount(account1.token, account1.iBan); + transactionService.depositIntoAccount(account1.iBan + "S", account1.cardNumber, account1.pin, 1000.0); + + testSavingsBalance(account1, 1000.0, 0.01); + + timeService.simulateTime(365); + + testSavingsBalance(account1, 1001.50, 0.01); + } + +} \ No newline at end of file