Added some small fixes, and start of tradeable asset editing page.
This commit is contained in:
parent
1097d0843f
commit
9dd0db14e0
|
@ -1,14 +1,17 @@
|
||||||
package nl.andrewl.coyotecredit.ctl;
|
package nl.andrewl.coyotecredit.ctl;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import nl.andrewl.coyotecredit.ctl.dto.AddSupportedTradeablePayload;
|
||||||
import nl.andrewl.coyotecredit.ctl.dto.EditExchangePayload;
|
import nl.andrewl.coyotecredit.ctl.dto.EditExchangePayload;
|
||||||
import nl.andrewl.coyotecredit.ctl.dto.InviteUserPayload;
|
import nl.andrewl.coyotecredit.ctl.dto.InviteUserPayload;
|
||||||
import nl.andrewl.coyotecredit.model.User;
|
import nl.andrewl.coyotecredit.model.User;
|
||||||
import nl.andrewl.coyotecredit.service.ExchangeService;
|
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
|
|
||||||
|
@ -83,4 +86,31 @@ public class ExchangeController {
|
||||||
exchangeService.edit(exchangeId, payload, user);
|
exchangeService.edit(exchangeId, payload, user);
|
||||||
return "redirect:/exchanges/" + exchangeId;
|
return "redirect:/exchanges/" + exchangeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/{exchangeId}/editTradeables")
|
||||||
|
public String getEditTradeablesPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
|
||||||
|
model.addAttribute("data", exchangeService.getEditTradeablesData(exchangeId, user));
|
||||||
|
return "exchange/edit_tradeables";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/{exchangeId}/addSupportedTradeable")
|
||||||
|
public String postAddSupportedTradeable(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddSupportedTradeablePayload payload) {
|
||||||
|
exchangeService.addSupportedTradeable(exchangeId, payload.tradeableId(), user);
|
||||||
|
return "redirect:/exchanges/" + exchangeId + "/editTradeables";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/{exchangeId}/removeSupportedTradeable/{tradeableId}")
|
||||||
|
public String getRemoveSupportedTradeablePage(@PathVariable long exchangeId, @PathVariable long tradeableId, @AuthenticationPrincipal User user) {
|
||||||
|
var data = exchangeService.getEditTradeablesData(exchangeId, user);
|
||||||
|
if (data.supportedPublicTradeables().stream().noneMatch(t -> t.id() == tradeableId)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This tradeable cannot be removed from the exchange.");
|
||||||
|
}
|
||||||
|
return "exchange/remove_supported_tradeable";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/{exchangeId}/removeSupportedTradeable/{tradeableId}")
|
||||||
|
public String postRemoveSupportedTradeable(@PathVariable long exchangeId, @PathVariable long tradeableId, @AuthenticationPrincipal User user) {
|
||||||
|
exchangeService.removeSupportedTradeable(exchangeId, tradeableId, user);
|
||||||
|
return "redirect:/exchanges/" + exchangeId + "/editTradeables";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
public record AddSupportedTradeablePayload(long tradeableId) {
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.coyotecredit.ctl.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record EditTradeablesData(
|
||||||
|
List<TradeableData> supportedPublicTradeables,
|
||||||
|
List<TradeableData> customTradeables,
|
||||||
|
TradeableData primaryTradeable,
|
||||||
|
List<TradeableData> eligiblePublicTradeables
|
||||||
|
) {}
|
|
@ -40,4 +40,10 @@ public class Balance {
|
||||||
this.tradeable = tradeable;
|
this.tradeable = tradeable;
|
||||||
this.amount = amount;
|
this.amount = amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o == this) return true;
|
||||||
|
return o instanceof Balance b && this.getBalanceId().equals(b.getBalanceId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,6 +97,10 @@ public class ExchangeService {
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public void ensureAdminAccount(long exchangeId, User user) {
|
public void ensureAdminAccount(long exchangeId, User user) {
|
||||||
|
getExchangeIfAdmin(exchangeId, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Exchange getExchangeIfAdmin(long exchangeId, User user) {
|
||||||
Exchange exchange = exchangeRepository.findById(exchangeId)
|
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||||
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
||||||
|
@ -104,6 +108,7 @@ public class ExchangeService {
|
||||||
if (!account.isAdmin()) {
|
if (!account.isAdmin()) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
return exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
@ -322,4 +327,66 @@ public class ExchangeService {
|
||||||
), true);
|
), true);
|
||||||
mailSender.send(msg);
|
mailSender.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public EditTradeablesData getEditTradeablesData(long exchangeId, User user) {
|
||||||
|
Exchange exchange = getExchangeIfAdmin(exchangeId, user);
|
||||||
|
List<TradeableData> supportedPublicTradeables = exchange.getSupportedTradeables().stream()
|
||||||
|
.map(TradeableData::new).sorted(Comparator.comparing(TradeableData::symbol)).toList();
|
||||||
|
List<TradeableData> customTradeables = exchange.getCustomTradeables().stream()
|
||||||
|
.map(TradeableData::new).sorted(Comparator.comparing(TradeableData::symbol)).toList();
|
||||||
|
List<TradeableData> eligiblePublicTradeables = tradeableRepository.findAllByExchangeNull().stream()
|
||||||
|
.filter(t -> !exchange.getSupportedTradeables().contains(t))
|
||||||
|
.map(TradeableData::new).sorted(Comparator.comparing(TradeableData::symbol)).toList();
|
||||||
|
return new EditTradeablesData(
|
||||||
|
supportedPublicTradeables,
|
||||||
|
customTradeables,
|
||||||
|
new TradeableData(exchange.getPrimaryTradeable()),
|
||||||
|
eligiblePublicTradeables
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void addSupportedTradeable(long exchangeId, long tradeableId, User user) {
|
||||||
|
Exchange exchange = getExchangeIfAdmin(exchangeId, user);
|
||||||
|
Tradeable tradeable = tradeableRepository.findById(tradeableId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable."));
|
||||||
|
// Exit if the tradeable is already supported.
|
||||||
|
if (exchange.getSupportedTradeables().contains(tradeable)) return;
|
||||||
|
if (tradeable.getExchange() != null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable.");
|
||||||
|
}
|
||||||
|
exchange.getSupportedTradeables().add(tradeable);
|
||||||
|
// Add a zero-value balance to any account that's missing it.
|
||||||
|
for (var acc : exchange.getAccounts()) {
|
||||||
|
Balance bal = acc.getBalanceForTradeable(tradeable);
|
||||||
|
if (bal == null) {
|
||||||
|
acc.getBalances().add(new Balance(acc, tradeable, BigDecimal.ZERO));
|
||||||
|
accountRepository.save(acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exchangeRepository.save(exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeSupportedTradeable(long exchangeId, long tradeableId, User user) {
|
||||||
|
Exchange exchange = getExchangeIfAdmin(exchangeId, user);
|
||||||
|
Tradeable tradeable = tradeableRepository.findById(tradeableId)
|
||||||
|
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable."));
|
||||||
|
// Quietly exit if the user is trying to remove a tradeable that isn't supported in the first place.
|
||||||
|
if (!exchange.getSupportedTradeables().contains(tradeable)) return;
|
||||||
|
if (exchange.getPrimaryTradeable().equals(tradeable)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot remove the primary tradeable asset. Change this first.");
|
||||||
|
}
|
||||||
|
exchange.getSupportedTradeables().remove(tradeable);
|
||||||
|
// Delete balance for any account that has it.
|
||||||
|
for (var acc : exchange.getAccounts()) {
|
||||||
|
Balance bal = acc.getBalanceForTradeable(tradeable);
|
||||||
|
if (bal != null) {
|
||||||
|
acc.getBalances().remove(bal);
|
||||||
|
accountRepository.save(acc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exchangeRepository.save(exchange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="account : ${accounts}">
|
<tr th:each="account : ${accounts}">
|
||||||
<td><a class="colored-link" th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
|
<td><a class="colored-link monospace" th:href="@{/accounts/{id}(id=${account.id()})}" th:text="${account.number()}"></a></td>
|
||||||
<td th:text="${account.name()}"></td>
|
<td th:text="${account.name()}"></td>
|
||||||
<td th:text="${account.admin()}"></td>
|
<td th:text="${account.admin()}"></td>
|
||||||
<td class="monospace" th:text="${account.totalBalance()}"></td>
|
<td class="monospace" th:text="${account.totalBalance()}"></td>
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
lang="en"
|
||||||
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
|
th:replace="~{layout/basic_page :: layout (title='Edit Tradeables', content=~{::#content})}"
|
||||||
|
>
|
||||||
|
<div id="content" class="container">
|
||||||
|
<h1 class="display-4">Edit Tradeable Assets</h1>
|
||||||
|
|
||||||
|
<div class="card text-white bg-dark mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Supported Publicly Traded Assets</h5>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
These assets are publicly available to all exchanges in Coyote Credit, and their price is updated automatically with real-world data.
|
||||||
|
</p>
|
||||||
|
<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 : ${data.supportedPublicTradeables()}">
|
||||||
|
<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="@{/exchanges/{eId}/removeSupportedTradeable/{tId}(eId=${exchangeId}, tId=${tradeable.id()})}">Remove</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<form
|
||||||
|
th:if="${!data.eligiblePublicTradeables().isEmpty()}"
|
||||||
|
th:action="@{/exchanges/{eId}/addSupportedTradeable(eId=${exchangeId})}"
|
||||||
|
method="post"
|
||||||
|
>
|
||||||
|
<label for="addSupportedTradeableSelect" class="form-label">Add support for a new tradeable asset</label>
|
||||||
|
<select id="addSupportedTradeableSelect" class="form-select" name="tradeableId">
|
||||||
|
<option
|
||||||
|
th:each="tradeable : ${data.eligiblePublicTradeables()}"
|
||||||
|
th:text="${tradeable.name() + ' (' + tradeable.symbol() + ')'}"
|
||||||
|
th:value="${tradeable.id()}"
|
||||||
|
></option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary mt-2" type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card text-white bg-dark mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Custom Traded Assets</h5>
|
||||||
|
<p class="card-text text-muted">
|
||||||
|
These assets are available only within this exchange.
|
||||||
|
</p>
|
||||||
|
<table class="table table-dark">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Price (USD)</th>
|
||||||
|
<th>Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="tradeable : ${data.customTradeables()}">
|
||||||
|
<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>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -24,6 +24,10 @@
|
||||||
<dd class="col-sm-6" th:text="${exchange.accountCount()}"></dd>
|
<dd class="col-sm-6" th:text="${exchange.accountCount()}"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
<a class="btn btn-primary" th:href="@{/accounts/{aId}(aId=${exchange.accountId()})}">My Account</a>
|
<a class="btn btn-primary" th:href="@{/accounts/{aId}(aId=${exchange.accountId()})}">My Account</a>
|
||||||
|
<span th:if="${exchange.accountAdmin()}">
|
||||||
|
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View All Accounts</a>
|
||||||
|
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/edit(eId=${exchange.id()})}">Edit Exchange Settings</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,29 +37,26 @@
|
||||||
<table class="table table-dark">
|
<table class="table table-dark">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Symbol</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Price (in USD)</th>
|
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Symbol</th>
|
||||||
|
<th>Price (USD)</th>
|
||||||
|
<th>Type</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="tradeable : ${exchange.supportedTradeables()}">
|
<tr th:each="tradeable : ${exchange.supportedTradeables()}">
|
||||||
|
<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 th:text="${tradeable.symbol()}"></td>
|
||||||
<td th:text="${tradeable.type()}"></td>
|
|
||||||
<td class="monospace" th:text="${tradeable.formattedPriceUsd()}"></td>
|
<td class="monospace" th:text="${tradeable.formattedPriceUsd()}"></td>
|
||||||
<td th:text="${tradeable.name()}"></td>
|
<td th:text="${tradeable.type()}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<span th:if="${exchange.accountAdmin()}">
|
||||||
</div>
|
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/editTradeables(eId=${exchange.id()})}">Edit Tradeable Assets</a>
|
||||||
|
</span>
|
||||||
<div class="card text-white bg-dark mb-3" th:if="${exchange.accountAdmin()}">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Administrator Tools</h5>
|
|
||||||
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View All Accounts</a>
|
|
||||||
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/edit(eId=${exchange.id()})}">Edit Exchange Settings</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!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 Supported Tradeable Asset</h1>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to remove this tradeable asset from your exchange?
|
||||||
|
All accounts will have their balance of this asset permanently removed,
|
||||||
|
and it cannot be undone.
|
||||||
|
</p>
|
||||||
|
<form th:action="@{/exchanges/{eId}/removeSupportedTradeable/{tId}(eId=${exchangeId}, tId=${tradeableId})}" method="post">
|
||||||
|
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/editTradeables(eId=${exchangeId})}">Cancel</a>
|
||||||
|
<button class="btn btn-danger" type="submit">Remove</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
Loading…
Reference in New Issue