Added transfers page, extra checks for transfer logic.
This commit is contained in:
parent
0448c5049c
commit
2640115e50
2
pom.xml
2
pom.xml
|
@ -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>
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 " +
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue