diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java b/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java index c934ce8..9ecdcda 100644 --- a/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/ExchangeController.java @@ -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"; + } } diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java new file mode 100644 index 0000000..d42bed3 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/AddSupportedTradeablePayload.java @@ -0,0 +1,4 @@ +package nl.andrewl.coyotecredit.ctl.dto; + +public record AddSupportedTradeablePayload(long tradeableId) { +} diff --git a/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java new file mode 100644 index 0000000..4db15c3 --- /dev/null +++ b/src/main/java/nl/andrewl/coyotecredit/ctl/dto/EditTradeablesData.java @@ -0,0 +1,10 @@ +package nl.andrewl.coyotecredit.ctl.dto; + +import java.util.List; + +public record EditTradeablesData( + List supportedPublicTradeables, + List customTradeables, + TradeableData primaryTradeable, + List eligiblePublicTradeables +) {} diff --git a/src/main/java/nl/andrewl/coyotecredit/model/Balance.java b/src/main/java/nl/andrewl/coyotecredit/model/Balance.java index 593db1b..d431007 100644 --- a/src/main/java/nl/andrewl/coyotecredit/model/Balance.java +++ b/src/main/java/nl/andrewl/coyotecredit/model/Balance.java @@ -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()); + } } diff --git a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java index 95ea989..0fbcc88 100644 --- a/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java +++ b/src/main/java/nl/andrewl/coyotecredit/service/ExchangeService.java @@ -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 supportedPublicTradeables = exchange.getSupportedTradeables().stream() + .map(TradeableData::new).sorted(Comparator.comparing(TradeableData::symbol)).toList(); + List customTradeables = exchange.getCustomTradeables().stream() + .map(TradeableData::new).sorted(Comparator.comparing(TradeableData::symbol)).toList(); + List 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); + } } diff --git a/src/main/resources/templates/exchange/accounts.html b/src/main/resources/templates/exchange/accounts.html index 1c40184..735b31b 100644 --- a/src/main/resources/templates/exchange/accounts.html +++ b/src/main/resources/templates/exchange/accounts.html @@ -19,7 +19,7 @@ - + diff --git a/src/main/resources/templates/exchange/edit_tradeables.html b/src/main/resources/templates/exchange/edit_tradeables.html new file mode 100644 index 0000000..62aeb4a --- /dev/null +++ b/src/main/resources/templates/exchange/edit_tradeables.html @@ -0,0 +1,86 @@ + + +
+

Edit Tradeable Assets

+ +
+
+
Supported Publicly Traded Assets
+

+ These assets are publicly available to all exchanges in Coyote Credit, and their price is updated automatically with real-world data. +

+ + + + + + + + + + + + + + + + + + + +
NameSymbolPrice (USD)TypeRemove
+ + + Remove +
+
+ + + +
+
+
+ +
+
+
Custom Traded Assets
+

+ These assets are available only within this exchange. +

+ + + + + + + + + + + + + + + + + +
NameSymbolPrice (USD)Type
+ +
+
+
+
diff --git a/src/main/resources/templates/exchange/exchange.html b/src/main/resources/templates/exchange/exchange.html index 1505c0a..ba5c550 100644 --- a/src/main/resources/templates/exchange/exchange.html +++ b/src/main/resources/templates/exchange/exchange.html @@ -24,6 +24,10 @@
My Account + + View All Accounts + Edit Exchange Settings + @@ -33,29 +37,26 @@ - - - + + + + - - +
SymbolTypePrice (in USD) NameSymbolPrice (USD)Type
+ +
- - - - \ No newline at end of file diff --git a/src/main/resources/templates/exchange/remove_supported_tradeable.html b/src/main/resources/templates/exchange/remove_supported_tradeable.html new file mode 100644 index 0000000..21c5c6a --- /dev/null +++ b/src/main/resources/templates/exchange/remove_supported_tradeable.html @@ -0,0 +1,18 @@ + + +
+

Remove Supported Tradeable Asset

+

+ 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. +

+
+ Cancel + +
+