Improved account transfer UI, added config-based api switch.
This commit is contained in:
parent
506a474819
commit
db2bcb576f
|
@ -1,7 +1,10 @@
|
|||
package nl.andrewl.coyotecredit.ctl.api;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.PublicAccountData;
|
||||
import nl.andrewl.coyotecredit.model.User;
|
||||
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
@ -16,7 +19,12 @@ public class ExchangeApiController {
|
|||
private final ExchangeService exchangeService;
|
||||
|
||||
@GetMapping(path = "/tradeables")
|
||||
public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId) {
|
||||
return exchangeService.getCurrentTradeables(exchangeId);
|
||||
public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
|
||||
return exchangeService.getCurrentTradeables(exchangeId, user);
|
||||
}
|
||||
|
||||
@GetMapping(path = "/accounts/{number}")
|
||||
public PublicAccountData getAccountData(@PathVariable long exchangeId, @PathVariable String number, @AuthenticationPrincipal User user) {
|
||||
return exchangeService.getPublicAccountData(exchangeId, number, user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewl.coyotecredit.ctl;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TransferPayload;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TransferPayload;
|
||||
import nl.andrewl.coyotecredit.model.User;
|
||||
import nl.andrewl.coyotecredit.service.AccountService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
@ -45,7 +45,7 @@ public class AccountPage {
|
|||
|
||||
@GetMapping(path = "/transfer")
|
||||
public String getTransferPage(Model model, @PathVariable long accountId, @AuthenticationPrincipal User user) {
|
||||
model.addAttribute("balances", accountService.getTransferData(accountId, user));
|
||||
model.addAttribute("data", accountService.getTransferData(accountId, user));
|
||||
return "account/transfer";
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewl.coyotecredit.ctl;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradePayload;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TradePayload;
|
||||
import nl.andrewl.coyotecredit.model.User;
|
||||
import nl.andrewl.coyotecredit.service.ExchangeService;
|
||||
import nl.andrewl.coyotecredit.service.TradeService;
|
||||
|
@ -26,7 +26,7 @@ public class TradePage {
|
|||
@AuthenticationPrincipal User user
|
||||
) {
|
||||
model.addAttribute("data", tradeService.getTradeData(accountId, user));
|
||||
return "trade";
|
||||
return "account/trade";
|
||||
}
|
||||
|
||||
@PostMapping
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record BalanceData(
|
||||
long id,
|
|
@ -1,7 +1,5 @@
|
|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.dto.SimpleAccountData;
|
||||
|
||||
public record ExchangeAccountData(
|
||||
ExchangeData exchange,
|
||||
SimpleAccountData account
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
/**
|
||||
* Account data that can be publicly seen by any member of an exchange.
|
||||
*/
|
||||
public record PublicAccountData(
|
||||
long id,
|
||||
String number,
|
||||
String name
|
||||
) {}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record SimpleAccountData (
|
||||
long id,
|
|
@ -1,4 +1,6 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
/**
|
||||
* The payload that's sent when a user performs a trade with the market. This
|
|
@ -1,5 +1,6 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||
import nl.andrewl.coyotecredit.model.Transaction;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
|
@ -1,5 +1,6 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||
import nl.andrewl.coyotecredit.model.Transfer;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
|
@ -0,0 +1,9 @@
|
|||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TransferPageData(
|
||||
long exchangeId,
|
||||
List<BalanceData> balances
|
||||
) {
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package nl.andrewl.coyotecredit.ctl.dto;
|
||||
package nl.andrewl.coyotecredit.ctl.exchange.dto;
|
||||
|
||||
public record TransferPayload (
|
||||
String recipientNumber,
|
|
@ -14,6 +14,7 @@ public interface AccountRepository extends JpaRepository<Account, Long> {
|
|||
List<Account> findAllByUser(User user);
|
||||
Optional<Account> findByNumber(String number);
|
||||
Optional<Account> findByUserAndExchange(User user, Exchange exchange);
|
||||
Optional<Account> findByNumberAndExchange(String number, Exchange exchange);
|
||||
boolean existsByUserAndExchange(User user, Exchange exchange);
|
||||
|
||||
List<Account> findAllByExchange(Exchange e);
|
||||
|
|
|
@ -2,7 +2,7 @@ package nl.andrewl.coyotecredit.service;
|
|||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.*;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.ExchangeData;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.*;
|
||||
import nl.andrewl.coyotecredit.dao.*;
|
||||
import nl.andrewl.coyotecredit.model.*;
|
||||
import nl.andrewl.coyotecredit.util.AccountNumberUtils;
|
||||
|
@ -34,13 +34,13 @@ public class AccountService {
|
|||
private final UserNotificationRepository notificationRepository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<BalanceData> getTransferData(long accountId, User user) {
|
||||
public TransferPageData getTransferData(long accountId, User user) {
|
||||
Account account = accountRepository.findById(accountId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!account.getUser().getId().equals(user.getId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return account.getBalances().stream()
|
||||
List<BalanceData> balances = account.getBalances().stream()
|
||||
.filter(b -> b.getAmount().compareTo(BigDecimal.ZERO) > 0)
|
||||
.map(b -> new BalanceData(
|
||||
b.getTradeable().getId(),
|
||||
|
@ -50,6 +50,7 @@ public class AccountService {
|
|||
))
|
||||
.sorted(Comparator.comparing(BalanceData::symbol))
|
||||
.toList();
|
||||
return new TransferPageData(account.getExchange().getId(), balances);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
|
@ -103,6 +103,18 @@ public class ExchangeService {
|
|||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PublicAccountData getPublicAccountData(long exchangeId, String number, User user) {
|
||||
Exchange exchange = exchangeRepository.findById(exchangeId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!accountRepository.existsByUserAndExchange(user, exchange)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Account account = accountRepository.findByNumberAndExchange(number, exchange)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
return new PublicAccountData(account.getId(), account.getNumber(), account.getName());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public void ensureAdminAccount(long exchangeId, User user) {
|
||||
getExchangeIfAdmin(exchangeId, user);
|
||||
|
@ -168,9 +180,12 @@ public class ExchangeService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<Long, String> getCurrentTradeables(long exchangeId) {
|
||||
public Map<Long, String> getCurrentTradeables(long exchangeId, User user) {
|
||||
Exchange e = exchangeRepository.findById(exchangeId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
if (!accountRepository.existsByUserAndExchange(user, e)) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Map<Long, String> tradeables = new HashMap<>();
|
||||
for (var t : e.getAllTradeables()) {
|
||||
tradeables.put(t.getId(), t.getMarketPriceUsd().toPlainString());
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package nl.andrewl.coyotecredit.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradeData;
|
||||
import nl.andrewl.coyotecredit.ctl.exchange.dto.TradeData;
|
||||
import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
|
||||
import nl.andrewl.coyotecredit.dao.AccountRepository;
|
||||
import nl.andrewl.coyotecredit.model.Account;
|
||||
|
|
|
@ -9,6 +9,7 @@ import nl.andrewl.coyotecredit.dao.TradeableRepository;
|
|||
import nl.andrewl.coyotecredit.model.Tradeable;
|
||||
import nl.andrewl.coyotecredit.model.TradeableType;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
@ -32,6 +33,7 @@ import java.util.concurrent.TimeUnit;
|
|||
@Slf4j
|
||||
public class TradeableUpdateService {
|
||||
private final TradeableRepository tradeableRepository;
|
||||
private final Environment environment;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
@ -57,7 +59,11 @@ public class TradeableUpdateService {
|
|||
for (var tradeable : publicTradeables) {
|
||||
// Special case of ignoring USD as the universal transfer currency.
|
||||
if (tradeable.getSymbol().equals("USD")) continue;
|
||||
executorService.schedule(() -> updateTradeable(tradeable), delay, TimeUnit.SECONDS);
|
||||
if (environment.getProperty("coyote-credit.enable-tradeable-updates", Boolean.TYPE, true)) {
|
||||
executorService.schedule(() -> updateTradeable(tradeable), delay, TimeUnit.SECONDS);
|
||||
} else {
|
||||
log.info("Tradeable update skipped for {}.", tradeable.getSymbol());
|
||||
}
|
||||
delay += POLYGON_API_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,4 @@ spring.mail.host=127.0.0.1
|
|||
spring.mail.port=1025
|
||||
|
||||
coyote-credit.base-url=http://localhost:8080
|
||||
coyote-credit.enable-tradeable-updates=false
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
const tradeableSelect = document.getElementById("tradeableSelect");
|
||||
const valueInput = document.getElementById("amountInput");
|
||||
const recipientNumberInput = document.getElementById("recipientNumberInput");
|
||||
const recipientNumberNote = document.getElementById("recipientNumberNote");
|
||||
|
||||
const exchangeId = document.getElementById("exchangeIdInput").value;
|
||||
const accountNumberRegex = new RegExp("\\d{4}-\\d{4}-\\d{4}-\\d{4}");
|
||||
|
||||
tradeableSelect.addEventListener("change", onSelectChanged);
|
||||
recipientNumberInput.addEventListener("change", onAccountNumberChanged);
|
||||
recipientNumberInput.addEventListener("keyup", onAccountNumberChanged);
|
||||
|
||||
function onSelectChanged() {
|
||||
valueInput.value = null;
|
||||
|
@ -17,3 +24,52 @@ function onSelectChanged() {
|
|||
valueInput.setAttribute("min", 0.0000000001);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the account number that was entered is valid.
|
||||
*/
|
||||
function onAccountNumberChanged() {
|
||||
const currentNumber = recipientNumberInput.value;
|
||||
console.log("Account number changed to " + currentNumber);
|
||||
if (currentNumber !== undefined && currentNumber.length > 0) {
|
||||
if (accountNumberRegex.test(currentNumber)) {
|
||||
recipientNumberNote.innerText = "Valid account number.";
|
||||
fetch("/api/exchanges/" + exchangeId + "/accounts/" + currentNumber)
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
response.json()
|
||||
.then(data => {
|
||||
console.log(data);
|
||||
showRecipientNumberNote("info", "Account number is valid. Will transfer to <span class='fw-bold'>" + data.name + "</span>");
|
||||
})
|
||||
.catch(() => {
|
||||
showRecipientNumberNote("warning", "Could not find an account with that number.");
|
||||
});
|
||||
} else if (response.status === 404) {
|
||||
showRecipientNumberNote("warning", "Could not find an account with that number.");
|
||||
} else {
|
||||
showRecipientNumberNote("warning", "Error: Couldn't read API response.");
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
showRecipientNumberNote("warning", "API error occurred. Couldn't fetch account information.");
|
||||
});
|
||||
} else {
|
||||
showRecipientNumberNote("warning", "Invalid account number. Format: <span class='monospace'>1234-1234-1234-1234</span>");
|
||||
}
|
||||
} else {
|
||||
showRecipientNumberNote("info", "Enter an account number.");
|
||||
}
|
||||
}
|
||||
|
||||
function showRecipientNumberNote(type, text) {
|
||||
recipientNumberNote.innerHTML = text;
|
||||
if (type === "warning") {
|
||||
recipientNumberNote.classList.add("text-danger");
|
||||
recipientNumberNote.classList.remove("text-muted");
|
||||
} else {
|
||||
recipientNumberNote.classList.remove("text-danger");
|
||||
recipientNumberNote.classList.add("text-muted");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,11 +10,12 @@
|
|||
Transfer funds to other accounts.
|
||||
</p>
|
||||
<form th:action="@{/accounts/{aId}/transfer(aId=${accountId})}" th:method="post">
|
||||
<input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
|
||||
<div class="mb-3">
|
||||
<label for="recipientNumberInput" class="form-label">Recipient Account Number</label>
|
||||
<input type="text" name="recipientNumber" class="form-control" id="recipientNumberInput" required/>
|
||||
<small class="text-muted">
|
||||
Format: <span class="monospace">1234-1234-1234-1234</span><br>
|
||||
<input type="text" name="recipientNumber" class="form-control monospace" id="recipientNumberInput" required/>
|
||||
<small class="text-muted text-danger" id="recipientNumberNote">
|
||||
Enter an account number.
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
@ -22,7 +23,7 @@
|
|||
<select class="form-select" id="tradeableSelect" name="tradeableId" required>
|
||||
<option value="" selected disabled hidden>Choose something to send</option>
|
||||
<option
|
||||
th:each="b : ${balances}"
|
||||
th:each="b : ${data.balances()}"
|
||||
th:text="${b.symbol() + ' - Balance ' + b.amount()}"
|
||||
th:value="${b.id()}"
|
||||
th:data-amount="${b.amount()}"
|
||||
|
|
Loading…
Reference in New Issue