Skip to content

Commit

Permalink
feat: routes for buy and sell stocks (#54)
Browse files Browse the repository at this point in the history
Co-authored-by: Izaias Machado <[email protected]>
  • Loading branch information
JonasFortes12 and izaiasmachado authored Mar 6, 2024
1 parent cc774b8 commit df5f1cd
Show file tree
Hide file tree
Showing 6 changed files with 385 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.mandacarubroker.controller;

import com.mandacarubroker.domain.position.RequestStockOwnershipDTO;
import com.mandacarubroker.domain.position.ResponseStockOwnershipDTO;
import com.mandacarubroker.service.PortfolioService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
Expand All @@ -35,14 +40,39 @@ public List<ResponseStockOwnershipDTO> getAuthenticatedUserStockPortfolio() {
return portfolioService.getAuthenticatedUserStockPortfolio();
}

@Operation(summary = "Comprar ações", description = "Compra um ou vários títulos de uma ações")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operação de compra efetuada")
})
@PostMapping("/stock/{stockId}/buy")
public ResponseEntity<ResponseStockOwnershipDTO> buyStock(
@PathVariable final String stockId,
@RequestBody @Valid final RequestStockOwnershipDTO shares) {
ResponseStockOwnershipDTO stockPosition = portfolioService.buyStock(stockId, shares);
return ResponseEntity.ok(stockPosition);
}

@Operation(summary = "Vender ações", description = "Vende um ou vários títulos de uma ação no portfolio")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Operação de venda efetuada")
})
@PostMapping("/stock/{stockId}/sell")
public ResponseEntity<ResponseStockOwnershipDTO> sellStock(
@PathVariable final String stockId,
@RequestBody @Valid final RequestStockOwnershipDTO shares) {
ResponseStockOwnershipDTO stockPosition = portfolioService.sellStock(stockId, shares);
return ResponseEntity.ok(stockPosition);
}

@Operation(summary = "Detalhes sobre a posse de ações de uma empresa", description = "Mostra detalhes sobre a posse de ações de uma empresa por um usuário autenticado")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Ação encontrada"),
@ApiResponse(responseCode = "404", description = "Ação não encontrada")
})
@GetMapping("/stock/{stockId}")
public ResponseStockOwnershipDTO getStockPositionById(@PathVariable final String stockId) {
Optional<ResponseStockOwnershipDTO> stockPosition = portfolioService.getAuthenticatedUserStockOwnershipByStockId(stockId);
Optional<ResponseStockOwnershipDTO> stockPosition = portfolioService
.getAuthenticatedUserStockOwnershipByStockId(stockId);

if (stockPosition.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Stock ownership not found");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,3 @@ public ResponseEntity<Object> deleteUser(@PathVariable final String id) {
return ResponseEntity.noContent().build();
}
}

165 changes: 150 additions & 15 deletions src/main/java/com/mandacarubroker/service/PortfolioService.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,61 @@
package com.mandacarubroker.service;

import com.mandacarubroker.domain.position.RequestStockOwnershipDTO;
import com.mandacarubroker.domain.position.ResponseStockOwnershipDTO;
import com.mandacarubroker.domain.position.StockOwnership;
import com.mandacarubroker.domain.position.StockOwnershipRepository;
import com.mandacarubroker.domain.stock.ResponseStockDTO;
import com.mandacarubroker.domain.stock.Stock;
import com.mandacarubroker.domain.stock.StockRepository;
import com.mandacarubroker.domain.user.User;
import com.mandacarubroker.domain.user.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;
import java.util.Optional;

@Service
public class PortfolioService {
private final StockOwnershipRepository stockPositionRepository;
private final StockService stockService;
private final UserRepository userRepository;
private final StockRepository stockRepository;


public PortfolioService(final StockOwnershipRepository receivedStockPositionRepository, final StockRepository receivedStockRepository) {
public PortfolioService(
final StockOwnershipRepository receivedStockPositionRepository,
final StockService receivedStockService,
final StockRepository receivedStockRepository,
final UserRepository recievedUserRepository) {
this.stockPositionRepository = receivedStockPositionRepository;
this.stockService = receivedStockService;
this.stockRepository = receivedStockRepository;
this.userRepository = recievedUserRepository;
}

public Optional<ResponseStockOwnershipDTO> getAuthenticatedUserStockOwnershipByStockId(final String stockId) {
User user = AuthService.getAuthenticatedUser();
return getStockOwnershipByStockId(user, stockId);
}

public Optional<ResponseStockOwnershipDTO> getStockOwnershipByStockId(final User user, final String stockId) {
String userId = user.getId();
Stock stock = stockRepository.findById(stockId).orElse(null);

if (stock == null) {
return Optional.empty();
}

StockOwnership stockPosition = stockPositionRepository.findByUserIdAndStockId(userId, stockId);

if (stockPosition == null) {
return Optional.of(ResponseStockOwnershipDTO.fromStock(stock));
}

ResponseStockOwnershipDTO responseStockOwnershipDTO = ResponseStockOwnershipDTO
.fromStockPosition(stockPosition);
return Optional.of(responseStockOwnershipDTO);
}

public List<ResponseStockOwnershipDTO> getAuthenticatedUserStockPortfolio() {
Expand All @@ -32,30 +68,129 @@ public List<ResponseStockOwnershipDTO> getPortfolioByUserId(final String userId)
List<StockOwnership> stockPositions = stockPositionRepository.findByUserId(userId);

return stockPositions.stream()
.map(ResponseStockOwnershipDTO::fromStockPosition)
.toList();
.map(ResponseStockOwnershipDTO::fromStockPosition)
.toList();
}

public Optional<ResponseStockOwnershipDTO> getAuthenticatedUserStockOwnershipByStockId(final String stockId) {
public Optional<ResponseStockOwnershipDTO> getStockPositionByUserIdAndStockId(final String stockId) {
User user = AuthService.getAuthenticatedUser();
return getStockOwnershipByStockId(user, stockId);
}

public Optional<ResponseStockOwnershipDTO> getStockOwnershipByStockId(final User user, final String stockId) {
String userId = user.getId();
Stock stock = stockRepository.findById(stockId).orElse(null);

if (stock == null) {
StockOwnership stockPosition = stockPositionRepository.findByUserIdAndStockId(userId, stockId);

if (stockPosition == null) {
return Optional.empty();
}

return Optional.of(ResponseStockOwnershipDTO.fromStockPosition(stockPosition));
}

private StockOwnership createStockPosition(
final RequestStockOwnershipDTO requestStockOwnershipDTO,
final ResponseStockDTO receivedStockDTO,
final User user) {
Stock stock = stockRepository.findById(receivedStockDTO.id()).orElse(null);
StockOwnership newStockPosition = new StockOwnership(requestStockOwnershipDTO, stock, user);
return stockPositionRepository.save(newStockPosition);
}

private Optional<StockOwnership> updateStockPositionShares(
final String userId,
final String stockId,
final RequestStockOwnershipDTO requestStockOwnershipDTO) {

StockOwnership stockPosition = stockPositionRepository.findByUserIdAndStockId(userId, stockId);
stockPosition.setShares(requestStockOwnershipDTO.shares());
return Optional.of(stockPositionRepository.save(stockPosition));
}

if (stockPosition == null) {
return Optional.of(ResponseStockOwnershipDTO.fromStock(stock));
private ResponseStockOwnershipDTO addStockPositionInPortfolio(
final ResponseStockDTO stock,
final User user,
final RequestStockOwnershipDTO shares) {

Optional<ResponseStockOwnershipDTO> existentStockPosition = getStockPositionByUserIdAndStockId(stock.id());

if (existentStockPosition.isPresent()) {
Optional<StockOwnership> updatedStockPosition = updateStockPositionShares(
user.getId(),
stock.id(),
new RequestStockOwnershipDTO(
existentStockPosition.get().totalShares()
+ shares.shares()));

if (updatedStockPosition.isEmpty()) {
throw new IllegalStateException("Error on update shares in portfolio");
}
return ResponseStockOwnershipDTO.fromStockPosition(
updatedStockPosition.get());
}

ResponseStockOwnershipDTO responseStockOwnershipDTO = ResponseStockOwnershipDTO.fromStockPosition(stockPosition);
return Optional.of(responseStockOwnershipDTO);
return ResponseStockOwnershipDTO.fromStockPosition(
createStockPosition(
new RequestStockOwnershipDTO(shares.shares()),
stock,
user));
}

private void removeStockPositionFromPortfolio(final String userId, final String stockId) {
StockOwnership stockPosition = stockPositionRepository.findByUserIdAndStockId(userId, stockId);
String stockPositionId = stockPosition.getId();
stockPositionRepository.deleteById(stockPositionId);
}

public ResponseStockOwnershipDTO buyStock(final String stockId, final RequestStockOwnershipDTO shares) {
User user = AuthService.getAuthenticatedUser();
Optional<ResponseStockDTO> stock = stockService.getStockById(stockId);

if (stock.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Stock not found");
}
double userBalance = user.getBalance();
double stockBoughtPrice = stock.get().price() * shares.shares();
if (userBalance < stockBoughtPrice) {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Insufficient balance");
}
ResponseStockOwnershipDTO boughtStockPosition = addStockPositionInPortfolio(stock.get(), user, shares);
user.setBalance(userBalance - stockBoughtPrice);
userRepository.save(user);
return boughtStockPosition;
}

public ResponseStockOwnershipDTO sellStock(final String stockId, final RequestStockOwnershipDTO shares) {
User user = AuthService.getAuthenticatedUser();
Optional<ResponseStockDTO> stock = stockService.getStockById(stockId);
Optional<ResponseStockOwnershipDTO> userStockPosition = getStockPositionByUserIdAndStockId(stockId);

if (userStockPosition.isEmpty() || stock.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Stock not found");
}
int sharesToSell = shares.shares();
int userShares = userStockPosition.get().totalShares();
if (sharesToSell > userShares) {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY,
"User with insufficient shares to sell");
}
double userBalance = user.getBalance();
double stocksSoldPrice = stock.get().price() * sharesToSell;
int remainingShares = userShares - sharesToSell;

Optional<StockOwnership> updatedStockPosition = updateStockPositionShares(
user.getId(),
stockId,
new RequestStockOwnershipDTO(
remainingShares));

if (updatedStockPosition.isEmpty()) {
throw new IllegalStateException("Error on update shares in portfolio");
}

if (remainingShares == 0) {
removeStockPositionFromPortfolio(user.getId(), stockId);
}
user.setBalance(userBalance + stocksSoldPrice);
userRepository.save(user);

return ResponseStockOwnershipDTO.fromStockPosition(updatedStockPosition.get());
}
}
1 change: 1 addition & 0 deletions src/main/java/com/mandacarubroker/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,4 @@ public Optional<ResponseUserDTO> findByUsername(final String username) {
return user.map(this::userToResponseUserDTO);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.mandacarubroker.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mandacarubroker.domain.auth.RequestAuthUserDTO;
import com.mandacarubroker.domain.auth.ResponseAuthUserDTO;
import com.mandacarubroker.domain.position.RequestStockOwnershipDTO;
import com.mandacarubroker.domain.stock.ResponseStockDTO;
import com.mandacarubroker.domain.user.RequestUserDTO;
import com.mandacarubroker.domain.user.User;
import com.mandacarubroker.service.AuthService;
import com.mandacarubroker.service.PortfolioService;
import com.mandacarubroker.service.StockService;
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.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.ResultMatcher;
import java.time.LocalDate;
import java.util.Optional;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc()
public class PortfolioControllerIT {

@MockBean
private PortfolioService portfolioService;

@Autowired
private MockMvc mockMvc;

@Autowired
private ObjectMapper objectMapper;

@Autowired
private StockService stockService;

@Autowired
private AuthService authService;

private final RequestUserDTO validRequestUserDTO = new RequestUserDTO(
"[email protected]",
"Marcos22",
"passmarco123",
"Marcos",
"Loiola",
LocalDate.of(2002, 2, 26),
0.25);
private final User validUser = new User(validRequestUserDTO);
private ResponseStockDTO stockToOperate;
private final RequestStockOwnershipDTO shares = new RequestStockOwnershipDTO(2);
private String urlRequestToBuyStock;
private String notFoundStockUrlRequestToBuyStock;
private String urlRequestToSellStock;

private RequestAuthUserDTO validRequestAuthUserDTO;

@BeforeEach
void setUp() {
validRequestAuthUserDTO = new RequestAuthUserDTO(
"admin",
"admin"
);
stockToOperate = stockService.getAllStocks().get(0);

urlRequestToBuyStock = "/portfolio/stock/" + stockToOperate.id() + "/buy";
urlRequestToSellStock = "/portfolio/stock/" + stockToOperate.id() + "/sell";
}

@AfterEach
void tearDown() {

}


@Test
void itShouldReturnOkStatusAfterSucessfulBuyStock() throws Exception{

Optional<ResponseAuthUserDTO> user = authService.login(validRequestAuthUserDTO);
String token = user.get().token();
String sharesJsonString = objectMapper.writeValueAsString(shares);

RequestBuilder requestBuilder = post(urlRequestToBuyStock)
.contentType("application/json")
.header("Authorization", token)
.content(sharesJsonString);
ResultMatcher matchStatus = status().isOk();

mockMvc.perform(requestBuilder).andExpect(matchStatus);
}

@Test
void itShouldReturnOkStatusAfterSucessfulSellStock() throws Exception{

Optional<ResponseAuthUserDTO> user = authService.login(validRequestAuthUserDTO);
String token = user.get().token();
String sharesJsonString = objectMapper.writeValueAsString(shares);

RequestBuilder requestBuilder = post(urlRequestToSellStock)
.contentType("application/json")
.header("Authorization", token)
.content(sharesJsonString);
ResultMatcher matchStatus = status().isOk();

mockMvc.perform(requestBuilder).andExpect(matchStatus);
}

}
Loading

0 comments on commit df5f1cd

Please sign in to comment.