Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

Upload files #60

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
18 changes: 18 additions & 0 deletions src/main/java/com/devexperts/account/Account.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devexperts.account;

import java.util.Objects;

public class Account {
private final AccountKey accountKey;
private final String firstName;
Expand Down Expand Up @@ -32,4 +34,20 @@ public Double getBalance() {
public void setBalance(Double balance) {
this.balance = balance;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Account account = (Account) o;
return Objects.equals(accountKey, account.accountKey) &&
Objects.equals(firstName, account.firstName) &&
Objects.equals(lastName, account.lastName) &&
Objects.equals(balance, account.balance);
}

@Override
public int hashCode() {
return Objects.hash(accountKey, firstName, lastName, balance);
}
}
17 changes: 16 additions & 1 deletion src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.devexperts.account;

import java.util.Objects;

/**
* Unique Account identifier
*
* <p>
* NOTE: we suspect that later {@link #accountId} is not going to be uniquely identifying an account,
* as we might add human-readable account representation and some clearing codes for partners.
* */
*/
public class AccountKey {
private final long accountId;

Expand All @@ -17,4 +19,17 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountKey that = (AccountKey) o;
return accountId == that.accountId;
}

@Override
public int hashCode() {
return Objects.hash(accountId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class AccountNotFoundException extends RuntimeException {
public AccountNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.exception;

public class InsufficientAccountBalanceException extends RuntimeException {
public InsufficientAccountBalanceException(String message) {
super(message);
}
}
29 changes: 27 additions & 2 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
package com.devexperts.rest;

import com.devexperts.exception.AccountNotFoundException;
import com.devexperts.exception.InsufficientAccountBalanceException;
import com.devexperts.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class AccountController extends AbstractAccountController {
private final AccountService accountService;

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
@Autowired
public AccountController(AccountService accountService) {
this.accountService = accountService;
}

@PostMapping("operations/transfer")
public ResponseEntity<Void> transfer(@RequestParam long sourceId,
@RequestParam long targetId,
@RequestParam double amount) {
try {
accountService.transfer(accountService.getAccount(sourceId), accountService.getAccount(targetId), amount);
return new ResponseEntity<>(HttpStatus.OK);
} catch (IllegalArgumentException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} catch (AccountNotFoundException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (InsufficientAccountBalanceException e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
59 changes: 49 additions & 10 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,75 @@

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.exception.AccountNotFoundException;
import com.devexperts.exception.InsufficientAccountBalanceException;
import org
.springframework.lang.NonNull;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();

private final Map<AccountKey, Account> accounts = new ConcurrentHashMap<>();

@Override
public void clear() {
accounts.clear();
}

@Override
public void createAccount(Account account) {
accounts.add(account);
public void createAccount(@NonNull Account account) {
if (account.getAccountKey() != null) {
if (accounts.putIfAbsent(account.getAccountKey(), account) != null) {
throw new IllegalArgumentException("Account already exists");
}
} else {
throw new IllegalArgumentException("AccountKey cannot be null");
}
}

@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.findAny()
.orElse(null);
return accounts.get(AccountKey.valueOf(id));
}


@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be greater than zero");
}

if (source == null || target == null) {
throw new AccountNotFoundException("One of the accounts was not found");
}

AccountKey sourceAccountKey = source.getAccountKey();
AccountKey targetAccountKey = target.getAccountKey();


AccountKey lock1 = sourceAccountKey.hashCode() < targetAccountKey.hashCode() ? sourceAccountKey : targetAccountKey;
AccountKey lock2 = sourceAccountKey.hashCode() < targetAccountKey.hashCode() ? targetAccountKey : sourceAccountKey;

synchronized (lock1) {
synchronized (lock2) {
Account sourceFromAccounts = accounts.get(sourceAccountKey);
Account targetFromAccounts = accounts.get(targetAccountKey);

if (sourceFromAccounts != null && targetFromAccounts != null) {
if (sourceFromAccounts.getBalance() < amount) {
throw new InsufficientAccountBalanceException("The source balance cannot be less than amount");
}
sourceFromAccounts.setBalance(sourceFromAccounts.getBalance() - amount);
targetFromAccounts.setBalance(targetFromAccounts.getBalance() + amount);
} else {
throw new AccountNotFoundException("One of the accounts was not found");
}
}
}
}
}
8 changes: 8 additions & 0 deletions src/main/resources/sql/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
create table accounts
(
id int8 not null,
first_name varchar(255) not null,
last_name varchar(255) not null,
balance int8 not null,
primary key (id)
);
5 changes: 5 additions & 0 deletions src/main/resources/sql/data/select.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
select source_id
from transfers
where transfer_time > '2019-01-01' :: timestamp
group by source_id
having sum(ammount) > 1000;
17 changes: 17 additions & 0 deletions src/main/resources/sql/data/transfers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
create table transfers
(
id int8 not null,
source_id int8 not null,
target_id int8 not null,
amount int8 not null,
transfer_time timestamp not null,
primary key (id)
);

alter table if exists transfers
add constraint transfers_source_fk
foreign key (source_id) references accounts;

alter table if exists transfers
add constraint transfers_target_fk
foreign key (target_id) references accounts;
87 changes: 87 additions & 0 deletions src/test/java/com/devexperts/rest/AccountControllerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.AccountService;
import org.junit.BeforeClass;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@AutoConfigureMockMvc
@SpringBootTest
class AccountControllerTest {
@Autowired
private MockMvc mockMvc;

@Autowired
private AccountService accountService;

private final String URL_TEMPLATE = "/api/operations/transfer";

@BeforeEach
public void createAccounts() {
accountService.createAccount(
new Account(AccountKey.valueOf(1), "John", "Statham", (double) 10000000));
accountService.createAccount(
new Account(AccountKey.valueOf(2), "Domenik", "Toretto", (double) 0));
}

@AfterEach
public void deleteAccounts() {
accountService.clear();
}

@Test
void transferFromOneAccountToAnotherThenStatusIsOK() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post(URL_TEMPLATE)
.param("sourceId", "1")
.param("targetId", "2")
.param("amount", "1337"))
.andDo(print())
.andExpect(status().isOk());
}

@Test
void transferFromOneAccountToAnotherWhenAmountIsInvalidThenStatusIsBadRequest() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post(URL_TEMPLATE)
.param("sourceId", "1")
.param("targetId", "2")
.param("amount", "-228"))
.andDo(print())
.andExpect(status().isBadRequest());
}

@Test
void transferFromOneAccountToAnotherWhenAccountNotExistThenStatusIsNotFound() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post(URL_TEMPLATE)
.param("sourceId", "1")
.param("targetId", "3")
.param("amount", "1337"))
.andDo(print())
.andExpect(status().isNotFound());
}

@Test
void transferFromOneAccountToAnotherWhenInsufficientAccountBalanceThenStatusIsInternalServerError() throws Exception {
this.mockMvc.perform(MockMvcRequestBuilders
.post(URL_TEMPLATE)
.param("sourceId", "2")
.param("targetId", "1")
.param("amount", "1337"))
.andDo(print())
.andExpect(status().isInternalServerError());
}
}
Loading