Added some small fixes, and start of tradeable asset editing page.

This commit is contained in:
Andrew Lalis 2022-02-22 10:55:33 +01:00
parent 1097d0843f
commit 9dd0db14e0
9 changed files with 236 additions and 14 deletions

View File

@ -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";
}
} }

View File

@ -0,0 +1,4 @@
package nl.andrewl.coyotecredit.ctl.dto;
public record AddSupportedTradeablePayload(long tradeableId) {
}

View File

@ -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
) {}

View File

@ -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());
}
} }

View File

@ -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);
}
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>