Added transfers page, extra checks for transfer logic.

This commit is contained in:
Andrew Lalis 2022-02-24 09:29:13 +01:00
parent 0448c5049c
commit 2640115e50
9 changed files with 136 additions and 4 deletions

View File

@ -10,7 +10,7 @@
</parent> </parent>
<groupId>nl.andrewl</groupId> <groupId>nl.andrewl</groupId>
<artifactId>coyotecredit</artifactId> <artifactId>coyotecredit</artifactId>
<version>1.0.0</version> <version>1.1.0</version>
<name>coyotecredit</name> <name>coyotecredit</name>
<description>Banking and stock trading application to teach students.</description> <description>Banking and stock trading application to teach students.</description>
<properties> <properties>

View File

@ -6,6 +6,9 @@ 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.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus; 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;
@ -113,4 +116,16 @@ public class ExchangeController {
exchangeService.removeSupportedTradeable(exchangeId, tradeableId, user); exchangeService.removeSupportedTradeable(exchangeId, tradeableId, user);
return "redirect:/exchanges/" + exchangeId + "/editTradeables"; return "redirect:/exchanges/" + exchangeId + "/editTradeables";
} }
@GetMapping(path = "/{exchangeId}/transfers")
public String getTransfers(
Model model,
@PathVariable long exchangeId,
@AuthenticationPrincipal User user,
@PageableDefault(size = 50, sort = "timestamp", direction = Sort.Direction.DESC)
Pageable pageable
) {
model.addAttribute("transfers", exchangeService.getTransfers(exchangeId, user, pageable));
return "exchange/transfers";
}
} }

View File

@ -0,0 +1,27 @@
package nl.andrewl.coyotecredit.ctl.dto;
import nl.andrewl.coyotecredit.model.Transfer;
import java.time.format.DateTimeFormatter;
public record TransferData(
long id,
String timestamp,
String senderNumber,
String recipientNumber,
TradeableData tradeable,
String amount,
String message
) {
public TransferData(Transfer t) {
this(
t.getId(),
t.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " UTC",
t.getSenderNumber(),
t.getRecipientNumber(),
new TradeableData(t.getTradeable()),
t.getAmount().toPlainString(),
t.getMessage()
);
}
}

View File

@ -4,11 +4,12 @@ import nl.andrewl.coyotecredit.model.Transfer;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface TransferRepository extends JpaRepository<Transfer, Long> { public interface TransferRepository extends JpaRepository<Transfer, Long>, JpaSpecificationExecutor<Transfer> {
@Query( @Query(
"SELECT t FROM Transfer t " + "SELECT t FROM Transfer t " +
"WHERE t.senderNumber = :accountNumber OR t.recipientNumber = :accountNumber " + "WHERE t.senderNumber = :accountNumber OR t.recipientNumber = :accountNumber " +

View File

@ -66,6 +66,9 @@ public class AccountService {
} }
Account recipient = accountRepository.findByNumber(recipientNumber) Account recipient = accountRepository.findByNumber(recipientNumber)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown recipient.")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown recipient."));
if (!recipient.getExchange().getId().equals(sender.getExchange().getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot transfer funds between exchanges.");
}
Tradeable tradeable = tradeableRepository.findById(payload.tradeableId()) Tradeable tradeable = tradeableRepository.findById(payload.tradeableId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable asset.")); .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown tradeable asset."));
BigDecimal amount = new BigDecimal(payload.amount()); BigDecimal amount = new BigDecimal(payload.amount());

View File

@ -7,6 +7,8 @@ import nl.andrewl.coyotecredit.model.*;
import nl.andrewl.coyotecredit.util.AccountNumberUtils; import nl.andrewl.coyotecredit.util.AccountNumberUtils;
import nl.andrewl.coyotecredit.util.StringUtils; import nl.andrewl.coyotecredit.util.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.mail.javamail.MimeMessageHelper;
@ -16,6 +18,9 @@ import org.springframework.web.server.ResponseStatusException;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.DecimalFormat; import java.text.DecimalFormat;
@ -29,6 +34,7 @@ public class ExchangeService {
private final ExchangeRepository exchangeRepository; private final ExchangeRepository exchangeRepository;
private final AccountRepository accountRepository; private final AccountRepository accountRepository;
private final TransactionRepository transactionRepository; private final TransactionRepository transactionRepository;
private final TransferRepository transferRepository;
private final TradeableRepository tradeableRepository; private final TradeableRepository tradeableRepository;
private final AccountValueSnapshotRepository accountValueSnapshotRepository; private final AccountValueSnapshotRepository accountValueSnapshotRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
@ -389,4 +395,21 @@ public class ExchangeService {
} }
exchangeRepository.save(exchange); exchangeRepository.save(exchange);
} }
@Transactional(readOnly = true)
public Page<TransferData> getTransfers(long exchangeId, User user, Pageable pageable) {
Exchange exchange = getExchangeIfAdmin(exchangeId, user);
Page<Transfer> transfers = transferRepository.findAll((root, query, criteriaBuilder) -> {
Subquery<String> accountNumberSubquery = query.subquery(String.class);
Root<Account> accountRoot = accountNumberSubquery.from(Account.class);
accountNumberSubquery.select(accountRoot.get("number"))
.distinct(true)
.where(criteriaBuilder.equal(accountRoot.get("exchange").get("id"), exchange.getId()));
return criteriaBuilder.or(
criteriaBuilder.in(root.get("senderNumber")).value(accountNumberSubquery),
criteriaBuilder.in(root.get("recipientNumber")).value(accountNumberSubquery)
);
}, pageable);
return transfers.map(TransferData::new);
}
} }

View File

@ -5,7 +5,7 @@
th:replace="~{layout/basic_page :: layout (title='Exchange Accounts', content=~{::#content})}" th:replace="~{layout/basic_page :: layout (title='Exchange Accounts', content=~{::#content})}"
> >
<div id="content" class="container"> <div id="content" class="container">
<h1>Accounts</h1> <h1 class="display-4">Accounts</h1>
<table class="table table-dark"> <table class="table table-dark">
<thead> <thead>

View File

@ -25,7 +25,8 @@
</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()}"> <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}/accounts(eId=${exchange.id()})}">View All Accounts</a>
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/transfers(eId=${exchange.id()}, page=0, size=50)}">View Transfers</a>
<a class="btn btn-secondary" th:href="@{/exchanges/{eId}/edit(eId=${exchange.id()})}">Edit Exchange Settings</a> <a class="btn btn-secondary" th:href="@{/exchanges/{eId}/edit(eId=${exchange.id()})}">Edit Exchange Settings</a>
</span> </span>
</div> </div>

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html
lang="en"
xmlns:th="http://www.thymeleaf.org"
th:replace="~{layout/basic_page :: layout (title='Transfers', content=~{::#content})}"
>
<div id="content" class="container">
<h1 class="display-4">Transfers</h1>
<p class="lead">
View all transfers between accounts within this exchange.
</p>
<div class="mb-3">
<span class="lead">
Page <span th:text="${transfers.getNumber() + 1}"></span> of <span th:text="${transfers.getTotalPages()}"></span>
<small class="text-muted">
(<span th:text="${transfers.getTotalElements()}"></span> total transfers)
</small>
</span>
<a
class="btn btn-primary"
th:if="${transfers.hasPrevious()}"
th:href="@{/exchanges/{eId}/transfers(eId=${exchangeId}, page=${transfers.previousPageable().getPageNumber()}, size=${transfers.previousPageable().getPageSize()})}"
>
Previous Page
</a>
<a
class="btn btn-primary"
th:if="${transfers.hasNext()}"
th:href="@{/exchanges/{eId}/transfers(eId=${exchangeId}, page=${transfers.nextPageable().getPageNumber()}, size=${transfers.nextPageable().getPageSize()})}"
>
Next Page
</a>
</div>
<table class="table table-dark">
<thead>
<tr>
<th>Id</th>
<th>Timestamp</th>
<th>Sender</th>
<th>Recipient</th>
<th>Asset</th>
<th>Amount</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr th:each="transfer : ${transfers.getContent()}">
<td class="monospace" th:text="${transfer.id()}"></td>
<td th:text="${transfer.timestamp()}"></td>
<td class="monospace" th:text="${transfer.senderNumber()}"></td>
<td class="monospace" th:text="${transfer.recipientNumber()}"></td>
<td>
<a class="colored-link" th:text="${transfer.tradeable().name()}" th:href="@{/tradeables/{tId}(tId=${transfer.tradeable().id()})}"></a>
</td>
<td class="monospace" th:text="${transfer.amount()}"></td>
<td class="small" th:text="${transfer.message()}"></td>
</tr>
</tbody>
</table>
</div>