Added admin functions for adding and removing tradeables.
This commit is contained in:
parent
db2bcb576f
commit
279f73af03
|
@ -0,0 +1,56 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.AddTradeablePayload;
|
||||||
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
|
import nl.andrewl.coyotecredit.service.TradeableService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(path = "/admin")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminController {
|
||||||
|
private final TradeableService tradeableService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public String get(@AuthenticationPrincipal User user) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
return "admin/admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/manageTradeables")
|
||||||
|
public String getManageTradeablesPage(Model model, @AuthenticationPrincipal User user) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
model.addAttribute("tradeables", tradeableService.getAllPublicTradeables(user));
|
||||||
|
return "admin/manage_tradeables";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/manageTradeables/add")
|
||||||
|
public String getAddTradeablePage(@AuthenticationPrincipal User user) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
return "admin/add_tradeable";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/manageTradeables/add")
|
||||||
|
public String addTradeable(@AuthenticationPrincipal User user, @ModelAttribute AddTradeablePayload payload) {
|
||||||
|
tradeableService.addTradeable(user, payload);
|
||||||
|
return "redirect:/admin/manageTradeables";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/manageTradeables/remove/{tradeableId}")
|
||||||
|
public String getRemoveTradeablePage(@AuthenticationPrincipal User user, @PathVariable long tradeableId) {
|
||||||
|
tradeableService.checkRemovePage(user, tradeableId);
|
||||||
|
return "admin/remove_tradeable";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/manageTradeables/remove/{tradeableId}")
|
||||||
|
public String removeTradeable(@AuthenticationPrincipal User user, @PathVariable long tradeableId) {
|
||||||
|
tradeableService.removeTradeable(user, tradeableId);
|
||||||
|
return "redirect:/admin/manageTradeables";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record AddTradeablePayload(String name, String symbol, String type, String description) {
|
||||||
|
}
|
|
@ -2,8 +2,10 @@ package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
import nl.andrewl.coyotecredit.model.Account;
|
import nl.andrewl.coyotecredit.model.Account;
|
||||||
import nl.andrewl.coyotecredit.model.Exchange;
|
import nl.andrewl.coyotecredit.model.Exchange;
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -18,4 +20,9 @@ public interface AccountRepository extends JpaRepository<Account, Long> {
|
||||||
boolean existsByUserAndExchange(User user, Exchange exchange);
|
boolean existsByUserAndExchange(User user, Exchange exchange);
|
||||||
|
|
||||||
List<Account> findAllByExchange(Exchange e);
|
List<Account> findAllByExchange(Exchange e);
|
||||||
|
|
||||||
|
@Query("SELECT a FROM Account a " +
|
||||||
|
"LEFT JOIN FETCH a.balances bal " +
|
||||||
|
"WHERE bal.tradeable = :t")
|
||||||
|
List<Account> findAllWithBalanceForTradeable(Tradeable t);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,4 +9,5 @@ import java.util.List;
|
||||||
@Repository
|
@Repository
|
||||||
public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
|
public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
|
||||||
List<Tradeable> findAllByExchangeNull();
|
List<Tradeable> findAllByExchangeNull();
|
||||||
|
boolean existsByExchangeNullAndSymbol(String symbol);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
package nl.andrewl.coyotecredit.dao;
|
package nl.andrewl.coyotecredit.dao;
|
||||||
|
|
||||||
|
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||||
import nl.andrewl.coyotecredit.model.Transaction;
|
import nl.andrewl.coyotecredit.model.Transaction;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
|
public interface TransactionRepository extends JpaRepository<Transaction, Long> {
|
||||||
Page<Transaction> findAllByAccountNumberOrderByTimestampDesc(String accountNumber, Pageable pageable);
|
Page<Transaction> findAllByAccountNumberOrderByTimestampDesc(String accountNumber, Pageable pageable);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("DELETE FROM Transaction tx WHERE tx.from = :t OR tx.to = :t")
|
||||||
|
void deleteAllWithTradeable(Tradeable t);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,27 @@
|
||||||
package nl.andrewl.coyotecredit.service;
|
package nl.andrewl.coyotecredit.service;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.AddTradeablePayload;
|
||||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||||
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
import nl.andrewl.coyotecredit.dao.*;
|
||||||
import nl.andrewl.coyotecredit.dao.TradeableRepository;
|
import nl.andrewl.coyotecredit.model.*;
|
||||||
import nl.andrewl.coyotecredit.model.Tradeable;
|
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TradeableService {
|
public class TradeableService {
|
||||||
private final TradeableRepository tradeableRepository;
|
private final TradeableRepository tradeableRepository;
|
||||||
private final AccountRepository accountRepository;
|
private final AccountRepository accountRepository;
|
||||||
|
private final ExchangeRepository exchangeRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
private final TradeableUpdateService tradeableUpdateService;
|
||||||
|
private final UserNotificationRepository notificationRepository;
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public TradeableData getTradeable(long id, User user) {
|
public TradeableData getTradeable(long id, User user) {
|
||||||
|
@ -26,4 +32,66 @@ public class TradeableService {
|
||||||
}
|
}
|
||||||
return new TradeableData(tradeable);
|
return new TradeableData(tradeable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public List<TradeableData> getAllPublicTradeables(User user) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
return tradeableRepository.findAllByExchangeNull()
|
||||||
|
.stream().map(TradeableData::new)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void addTradeable(User user, AddTradeablePayload payload) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
if (tradeableRepository.existsByExchangeNullAndSymbol(payload.symbol())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Tradeable with that symbol already exists.");
|
||||||
|
}
|
||||||
|
TradeableType type;
|
||||||
|
try {
|
||||||
|
type = TradeableType.valueOf(payload.type());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid type.", e);
|
||||||
|
}
|
||||||
|
Tradeable tradeable = new Tradeable(payload.symbol(), type, payload.name(), payload.description(), BigDecimal.ZERO, null);
|
||||||
|
tradeable = tradeableRepository.save(tradeable);
|
||||||
|
tradeableUpdateService.updateTradeable(tradeable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public void checkRemovePage(User user, long tradeableId) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
Tradeable t = tradeableRepository.findById(tradeableId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (t.getExchange() != null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
if (t.getSymbol().equalsIgnoreCase("USD")) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeTradeable(User user, long tradeableId) {
|
||||||
|
if (!user.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
|
Tradeable t = tradeableRepository.findById(tradeableId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
if (t.getExchange() != null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove exchange-custom tradeable.");
|
||||||
|
if (t.getSymbol().equalsIgnoreCase("USD")) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove USD.");
|
||||||
|
for (Account account : accountRepository.findAllWithBalanceForTradeable(t)) {
|
||||||
|
Balance bal = account.getBalanceForTradeable(t);
|
||||||
|
if (bal != null) {
|
||||||
|
account.getBalances().remove(bal);
|
||||||
|
notificationRepository.save(new UserNotification(
|
||||||
|
account.getUser(),
|
||||||
|
String.format("Your balance of %s %s in %s has been removed because Coyote Credit has removed support for it.",
|
||||||
|
bal.getAmount().toPlainString(),
|
||||||
|
t.getSymbol(),
|
||||||
|
account.getExchange().getName())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transactionRepository.deleteAllWithTradeable(t);
|
||||||
|
for (Exchange exchange : exchangeRepository.findAll()) {
|
||||||
|
exchange.getSupportedTradeables().remove(t);
|
||||||
|
exchangeRepository.save(exchange);
|
||||||
|
}
|
||||||
|
tradeableRepository.delete(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ public class TradeableUpdateService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTradeable(Tradeable tradeable) {
|
public void updateTradeable(Tradeable tradeable) {
|
||||||
BigDecimal updatedValue = null;
|
BigDecimal updatedValue = null;
|
||||||
if (tradeable.getType().equals(TradeableType.STOCK)) {
|
if (tradeable.getType().equals(TradeableType.STOCK)) {
|
||||||
updatedValue = fetchStockClosePrice(tradeable.getSymbol());
|
updatedValue = fetchStockClosePrice(tradeable.getSymbol());
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Add Tradeable', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Add Tradeable Asset</h1>
|
||||||
|
<p class="lead">Add a new publicly tradeable asset for exchanges to use.</p>
|
||||||
|
<form th:action="@{/admin/manageTradeables/add}" method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="nameInput" class="form-label">Name</label>
|
||||||
|
<input id="nameInput" name="name" class="form-control" type="text" required/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="symbolInput" class="form-label">Symbol</label>
|
||||||
|
<input id="symbolInput" name="symbol" class="form-control" type="text" required/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="typeSelect" class="form-label">Type</label>
|
||||||
|
<select id="typeSelect" name="type" class="form-select" required>
|
||||||
|
<option>FIAT</option>
|
||||||
|
<option>CRYPTO</option>
|
||||||
|
<option>STOCK</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="descriptionInput" class="form-label">Description</label>
|
||||||
|
<textarea id="descriptionInput" name="description" class="form-control" rows="3" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Admin', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Admin</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Control top-level application settings and data within this admin interface.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a class="btn btn-primary" th:href="@{/admin/manageTradeables}">Manage Tradeable Assets</a>
|
||||||
|
</div>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Manage Tradeables', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Manage Tradeable Assets</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Manage the global collection of tradeable assets that exchanges may incorporate into their
|
||||||
|
systems.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="card text-white bg-dark mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Tradeable Assets</h5>
|
||||||
|
<table class="table table-dark">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Price (USD)</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="tradeable : ${tradeables}">
|
||||||
|
<td>
|
||||||
|
<a class="colored-link" th:href="@{/tradeables/{tId}(tId=${tradeable.id()})}" th:text="${tradeable.name()}"></a>
|
||||||
|
</td>
|
||||||
|
<td th:text="${tradeable.symbol()}"></td>
|
||||||
|
<td class="monospace" th:text="${tradeable.formattedPriceUsd()}"></td>
|
||||||
|
<td th:text="${tradeable.type()}"></td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
class="colored-link"
|
||||||
|
th:href="@{/admin/manageTradeables/remove/{tId}(tId=${tradeable.id()})}"
|
||||||
|
th:if="${!tradeable.symbol().equalsIgnoreCase('USD')}"
|
||||||
|
>Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<span>
|
||||||
|
<a class="btn btn-primary" th:href="@{/admin/manageTradeables/add}">Add</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Remove Tradeable', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Remove Tradeable</h1>
|
||||||
|
<p class="lead">
|
||||||
|
Are you sure you want to remove this tradeable? It will be removed from all exchanges, and
|
||||||
|
all accounts will have their balance of this tradeable removed permanently.
|
||||||
|
</p>
|
||||||
|
<form th:action="@{/admin/manageTradeables/remove/{tId}(tId=${tradeableId})}" method="post">
|
||||||
|
<button type="submit" class="btn btn-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
<a th:href="@{/admin/manageTradeables}" class="btn btn-secondary">Cancel</a>
|
||||||
|
</div>
|
|
@ -36,6 +36,9 @@
|
||||||
></span>
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" th:if="${#authentication.getPrincipal().isAdmin()}">
|
||||||
|
<a class="nav-link" th:href="@{/admin}">Admin</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
<form class="d-flex" th:action="@{/logout}" th:method="post">
|
||||||
<button class="btn btn-dark" type="submit">Logout</button>
|
<button class="btn btn-dark" type="submit">Logout</button>
|
||||||
|
|
Loading…
Reference in New Issue