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