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;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.AddSupportedTradeablePayload;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.EditExchangePayload;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.InviteUserPayload;
|
||||
import nl.andrewl.coyotecredit.model.User;
|
||||
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||
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;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
|
@ -83,4 +86,31 @@ public class ExchangeController {
|
|||
exchangeService.edit(exchangeId, payload, user);
|
||||
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.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)
|
||||
public void ensureAdminAccount(long exchangeId, User user) {
|
||||
getExchangeIfAdmin(exchangeId, user);
|
||||
}
|
||||
|
||||
private Exchange getExchangeIfAdmin(long exchangeId, User user) {
|
||||
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
Account account = accountRepository.findByUserAndExchange(user, exchange)
|
||||
|
@ -104,6 +108,7 @@ public class ExchangeService {
|
|||
if (!account.isAdmin()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return exchange;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
@ -322,4 +327,66 @@ public class ExchangeService {
|
|||
), true);
|
||||
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>
|
||||
<tbody>
|
||||
<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.admin()}"></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>
|
||||
</dl>
|
||||
<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>
|
||||
|
||||
|
@ -33,29 +37,26 @@
|
|||
<table class="table table-dark">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Symbol</th>
|
||||
<th>Type</th>
|
||||
<th>Price (in USD)</th>
|
||||
<th>Name</th>
|
||||
<th>Symbol</th>
|
||||
<th>Price (USD)</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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.type()}"></td>
|
||||
<td class="monospace" th:text="${tradeable.formattedPriceUsd()}"></td>
|
||||
<td th:text="${tradeable.name()}"></td>
|
||||
<td th:text="${tradeable.type()}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span th:if="${exchange.accountAdmin()}">
|
||||
<a class="btn btn-primary" th:href="@{/exchanges/{eId}/editTradeables(eId=${exchange.id()})}">Edit Tradeable Assets</a>
|
||||
</span>
|
||||
</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