Added working first stuff.
This commit is contained in:
		
							parent
							
								
									5ffe9b3a84
								
							
						
					
					
						commit
						0538334df4
					
				| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Configuration;
 | 
				
			||||||
 | 
					import org.springframework.http.CacheControl;
 | 
				
			||||||
 | 
					import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 | 
				
			||||||
 | 
					import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Configuration
 | 
				
			||||||
 | 
					public class WebMvcConfig implements WebMvcConfigurer {
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void addResourceHandlers(ResourceHandlerRegistry registry) {
 | 
				
			||||||
 | 
							registry.addResourceHandler("/static/**")
 | 
				
			||||||
 | 
									.addResourceLocations("classpath:/static/")
 | 
				
			||||||
 | 
									.setOptimizeLocations(true)
 | 
				
			||||||
 | 
									.setCacheControl(CacheControl.maxAge(Duration.ofHours(1)));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 | 
				
			||||||
		http
 | 
							http
 | 
				
			||||||
			.authorizeRequests()
 | 
								.authorizeRequests()
 | 
				
			||||||
				.antMatchers(
 | 
									.antMatchers(
 | 
				
			||||||
						"/login", "/login/processing"
 | 
											"/login", "/login/processing", "/static/**"
 | 
				
			||||||
				).permitAll()
 | 
									).permitAll()
 | 
				
			||||||
				.and()
 | 
									.and()
 | 
				
			||||||
			.authorizeRequests().anyRequest().authenticated()
 | 
								.authorizeRequests().anyRequest().authenticated()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,18 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.ctl;
 | 
					package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
import nl.andrewl.coyotecredit.service.AccountService;
 | 
					import nl.andrewl.coyotecredit.service.AccountService;
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
import org.springframework.ui.Model;
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
					import org.springframework.util.MultiValueMap;
 | 
				
			||||||
import org.springframework.web.bind.annotation.PathVariable;
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
import org.springframework.web.bind.annotation.RequestMapping;
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Controller
 | 
					@Controller
 | 
				
			||||||
@RequestMapping(path = "/accounts/{accountId}")
 | 
					@RequestMapping(path = "/accounts/{accountId}")
 | 
				
			||||||
| 
						 | 
					@ -26,4 +30,18 @@ public class AccountPage {
 | 
				
			||||||
		model.addAttribute("account", data);
 | 
							model.addAttribute("account", data);
 | 
				
			||||||
		return "account";
 | 
							return "account";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/editBalances")
 | 
				
			||||||
 | 
						public String getEditBalancesPage(Model model, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							var data = accountService.getAccountData(user, accountId);
 | 
				
			||||||
 | 
							if (!data.userAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							model.addAttribute("account", data);
 | 
				
			||||||
 | 
							return "account/edit_balances";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/editBalances")
 | 
				
			||||||
 | 
						public String postEditBalances(@PathVariable long accountId, @AuthenticationPrincipal User user, @RequestParam MultiValueMap<String, String> paramMap) {
 | 
				
			||||||
 | 
							accountService.editBalances(accountId, user, paramMap);
 | 
				
			||||||
 | 
							return "redirect:/accounts/" + accountId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.AddAccountPayload;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
				
			||||||
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Controller;
 | 
				
			||||||
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller
 | 
				
			||||||
 | 
					@RequestMapping(path = "/exchanges/{exchangeId}")
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class ExchangePage {
 | 
				
			||||||
 | 
						private final ExchangeService exchangeService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping
 | 
				
			||||||
 | 
						public String get(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							model.addAttribute("exchange", exchangeService.getData(exchangeId, user));
 | 
				
			||||||
 | 
							return "exchange";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/accounts")
 | 
				
			||||||
 | 
						public String getAccountsPage(Model model, @PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							model.addAttribute("accounts", exchangeService.getAccounts(exchangeId, user));
 | 
				
			||||||
 | 
							return "exchange/accounts";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/addAccount")
 | 
				
			||||||
 | 
						public String getAddAccountPage(@PathVariable long exchangeId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							exchangeService.ensureAdminAccount(exchangeId, user);
 | 
				
			||||||
 | 
							return "exchange/addAccount";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/addAccount")
 | 
				
			||||||
 | 
						public String postAddAcount(@PathVariable long exchangeId, @AuthenticationPrincipal User user, @ModelAttribute AddAccountPayload payload) {
 | 
				
			||||||
 | 
							long accountId = exchangeService.addAccount(exchangeId, user, payload);
 | 
				
			||||||
 | 
							return "redirect:/accounts/" + accountId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/removeAccount/{accountId}")
 | 
				
			||||||
 | 
						public String getRemoveAccountPage(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							exchangeService.ensureAdminAccount(exchangeId, user);
 | 
				
			||||||
 | 
							return "exchange/removeAccount";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping(path = "/removeAccount/{accountId}")
 | 
				
			||||||
 | 
						public String postRemoveAccount(@PathVariable long exchangeId, @PathVariable long accountId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							exchangeService.removeAccount(exchangeId, accountId, user);
 | 
				
			||||||
 | 
							return "redirect:/exchanges/" + exchangeId + "/accounts";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,41 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.TradePayload;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.service.TradeService;
 | 
				
			||||||
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Controller;
 | 
				
			||||||
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.validation.Valid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller
 | 
				
			||||||
 | 
					@RequestMapping(path = "/trade/{accountId}")
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class TradePage {
 | 
				
			||||||
 | 
						private final TradeService tradeService;
 | 
				
			||||||
 | 
						private final ExchangeService exchangeService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping
 | 
				
			||||||
 | 
						public String get(
 | 
				
			||||||
 | 
								Model model,
 | 
				
			||||||
 | 
								@PathVariable long accountId,
 | 
				
			||||||
 | 
								@AuthenticationPrincipal User user
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							model.addAttribute("data", tradeService.getTradeData(accountId, user));
 | 
				
			||||||
 | 
							return "trade";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping
 | 
				
			||||||
 | 
						public String doTrade(
 | 
				
			||||||
 | 
								@PathVariable long accountId,
 | 
				
			||||||
 | 
								@AuthenticationPrincipal User user,
 | 
				
			||||||
 | 
								@ModelAttribute @Valid TradePayload payload
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							exchangeService.doTrade(accountId, payload, user);
 | 
				
			||||||
 | 
							return "redirect:/accounts/" + accountId;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.service.UserService;
 | 
				
			||||||
 | 
					import org.springframework.security.core.annotation.AuthenticationPrincipal;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Controller;
 | 
				
			||||||
 | 
					import org.springframework.ui.Model;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.GetMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.PathVariable;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestMapping;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Controller
 | 
				
			||||||
 | 
					@RequestMapping(path = "/users/{userId}")
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class UserPage {
 | 
				
			||||||
 | 
						private final UserService userService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping
 | 
				
			||||||
 | 
						public String get(Model model, @PathVariable long userId, @AuthenticationPrincipal User user) {
 | 
				
			||||||
 | 
							model.addAttribute("user", userService.getUser(userId, user));
 | 
				
			||||||
 | 
							return "user";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.service.ExchangeService;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.GetMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.PathVariable;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RequestMapping;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RestController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					@RequestMapping("/api/exchanges/{exchangeId}")
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class ExchangeApiController {
 | 
				
			||||||
 | 
						private final ExchangeService exchangeService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping(path = "/tradeables")
 | 
				
			||||||
 | 
						public Map<Long, String> getCurrentTradeables(@PathVariable long exchangeId) {
 | 
				
			||||||
 | 
							return exchangeService.getCurrentTradeables(exchangeId);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record AddAccountPayload(
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String username,
 | 
				
			||||||
 | 
							String password
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record BalanceData(
 | 
				
			||||||
 | 
						long id,
 | 
				
			||||||
 | 
						String symbol,
 | 
				
			||||||
 | 
						String amount
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ExchangeData(
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String primaryTradeable
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The data that's needed for displaying a user's account page.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record FullAccountData (
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String number,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							boolean admin,
 | 
				
			||||||
 | 
							boolean userAdmin,
 | 
				
			||||||
 | 
							ExchangeData exchange,
 | 
				
			||||||
 | 
							List<BalanceData> balances,
 | 
				
			||||||
 | 
							String totalBalance,
 | 
				
			||||||
 | 
							List<TransactionData> recentTransactions
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The data that's used on the exchange page.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record FullExchangeData (
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							TradeableData primaryTradeable,
 | 
				
			||||||
 | 
							List<TradeableData> supportedTradeables,
 | 
				
			||||||
 | 
							// Account info
 | 
				
			||||||
 | 
							boolean accountAdmin
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record SimpleAccountData (
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String number,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							boolean admin,
 | 
				
			||||||
 | 
							String totalBalance
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record TradeData(
 | 
				
			||||||
 | 
							long accountId,
 | 
				
			||||||
 | 
							String accountNumber,
 | 
				
			||||||
 | 
							long exchangeId,
 | 
				
			||||||
 | 
							List<TradeableData> tradeablesToSell,
 | 
				
			||||||
 | 
							Map<String, BigDecimal> accountBalances,
 | 
				
			||||||
 | 
							List<TradeableData> tradeablesToBuy
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The payload that's sent when a user performs a trade with the market. This
 | 
				
			||||||
 | 
					 * can be either a SELL or a BUY trade.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     In a SELL trade, the user indicates an amount of their selling tradeable
 | 
				
			||||||
 | 
					 *     to sell in exchange for the market-equivalent value of the buying
 | 
				
			||||||
 | 
					 *     tradeable.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     In a BUY trade, the opposite happens, where the user indicates how much
 | 
				
			||||||
 | 
					 *     of the buying tradeable they want to acquire, and will have a market-
 | 
				
			||||||
 | 
					 *     equivalent value deducted from their selling tradeable.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record TradePayload(
 | 
				
			||||||
 | 
							String type, // SELL or BUY
 | 
				
			||||||
 | 
							long sellTradeableId,
 | 
				
			||||||
 | 
							long buyTradeableId,
 | 
				
			||||||
 | 
							String value
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,23 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Tradeable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record TradeableData(
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String symbol,
 | 
				
			||||||
 | 
							String type,
 | 
				
			||||||
 | 
							String marketPriceUsd,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String description
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						public TradeableData(Tradeable t) {
 | 
				
			||||||
 | 
							this(
 | 
				
			||||||
 | 
									t.getId(),
 | 
				
			||||||
 | 
									t.getSymbol(),
 | 
				
			||||||
 | 
									t.getType().name(),
 | 
				
			||||||
 | 
									t.getMarketPriceUsd().toPlainString(),
 | 
				
			||||||
 | 
									t.getName(),
 | 
				
			||||||
 | 
									t.getDescription()
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Transaction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.format.DateTimeFormatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record TransactionData(
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							TradeableData from,
 | 
				
			||||||
 | 
							String fromAmount,
 | 
				
			||||||
 | 
							TradeableData to,
 | 
				
			||||||
 | 
							String toAmount,
 | 
				
			||||||
 | 
							String timestamp
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						public TransactionData(Transaction t) {
 | 
				
			||||||
 | 
							this(
 | 
				
			||||||
 | 
									t.getId(),
 | 
				
			||||||
 | 
									new TradeableData(t.getFrom()),
 | 
				
			||||||
 | 
									t.getFromAmount().toPlainString(),
 | 
				
			||||||
 | 
									new TradeableData(t.getTo()),
 | 
				
			||||||
 | 
									t.getToAmount().toPlainString(),
 | 
				
			||||||
 | 
									t.getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.ctl.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record UserData (
 | 
				
			||||||
 | 
							long id,
 | 
				
			||||||
 | 
							String username
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,17 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.dao;
 | 
					package nl.andrewl.coyotecredit.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Account;
 | 
					import nl.andrewl.coyotecredit.model.Account;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Exchange;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
import org.springframework.data.jpa.repository.JpaRepository;
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
import org.springframework.stereotype.Repository;
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
public interface AccountRepository extends JpaRepository<Account, Long> {
 | 
					public interface AccountRepository extends JpaRepository<Account, Long> {
 | 
				
			||||||
	List<Account> findAllByUser(User user);
 | 
						List<Account> findAllByUser(User user);
 | 
				
			||||||
 | 
						Optional<Account> findByNumber(String number);
 | 
				
			||||||
 | 
						Optional<Account> findByUserAndExchange(User user, Exchange exchange);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Exchange;
 | 
				
			||||||
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					public interface ExchangeRepository extends JpaRepository<Exchange, Long> {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Tradeable;
 | 
				
			||||||
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					public interface TradeableRepository extends JpaRepository<Tradeable, Long> {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Transaction;
 | 
				
			||||||
 | 
					import org.springframework.data.domain.Page;
 | 
				
			||||||
 | 
					import org.springframework.data.domain.Pageable;
 | 
				
			||||||
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Repository
 | 
				
			||||||
 | 
					public interface TransactionRepository extends JpaRepository<Transaction, Long> {
 | 
				
			||||||
 | 
						Page<Transaction> findAllByAccountNumberOrderByTimestampDesc(String accountNumber, Pageable pageable);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,9 @@ import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.math.RoundingMode;
 | 
				
			||||||
import java.util.HashMap;
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,23 +24,72 @@ public class Account {
 | 
				
			||||||
	@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
						@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
				
			||||||
	private Long id;
 | 
						private Long id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The unique account number.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	@Column(nullable = false, unique = true)
 | 
						@Column(nullable = false, unique = true)
 | 
				
			||||||
	private String number;
 | 
						private String number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The user that this account belongs to.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	@ManyToOne(fetch = FetchType.LAZY, optional = false)
 | 
						@ManyToOne(fetch = FetchType.LAZY, optional = false)
 | 
				
			||||||
	private User user;
 | 
						private User user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The name on this account.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Column
 | 
				
			||||||
 | 
						private String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The exchange that this account belongs to.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	@ManyToOne(fetch = FetchType.LAZY, optional = false)
 | 
						@ManyToOne(fetch = FetchType.LAZY, optional = false)
 | 
				
			||||||
	private Exchange exchange;
 | 
						private Exchange exchange;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Whether this account is an administrator in the exchange it's linked to.
 | 
				
			||||||
 | 
						 * Administrators have special permissions to add and remove other accounts,
 | 
				
			||||||
 | 
						 * custom tradeables, exchange rates, and more.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private boolean admin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The set of tradeable balances that this account has.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
 | 
						@OneToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
 | 
				
			||||||
	private Set<Balance> balances;
 | 
						private Set<Balance> balances;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public Map<Currency, BigDecimal> getMappedBalances() {
 | 
						public Account(String number, User user, String name, Exchange exchange) {
 | 
				
			||||||
		Map<Currency, BigDecimal> b = new HashMap<>();
 | 
							this.number = number;
 | 
				
			||||||
 | 
							this.user = user;
 | 
				
			||||||
 | 
							this.name = name;
 | 
				
			||||||
 | 
							this.exchange = exchange;
 | 
				
			||||||
 | 
							this.balances = new HashSet<>();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Map<Tradeable, BigDecimal> getMappedBalances() {
 | 
				
			||||||
 | 
							Map<Tradeable, BigDecimal> b = new HashMap<>();
 | 
				
			||||||
		for (var bal : getBalances()) {
 | 
							for (var bal : getBalances()) {
 | 
				
			||||||
			b.put(bal.getCurrency(), bal.getAmount());
 | 
								b.put(bal.getTradeable(), bal.getAmount());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return b;
 | 
							return b;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Balance getBalanceForTradeable(Tradeable t) {
 | 
				
			||||||
 | 
							for (var bal : getBalances()) {
 | 
				
			||||||
 | 
								if (bal.getTradeable().equals(t)) return bal;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public BigDecimal getTotalBalance() {
 | 
				
			||||||
 | 
							BigDecimal totalUsd = new BigDecimal(0);
 | 
				
			||||||
 | 
							for (var bal : getBalances()) {
 | 
				
			||||||
 | 
								totalUsd = totalUsd.add(bal.getTradeable().getMarketPriceUsd().multiply(bal.getAmount()));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return totalUsd.divide(getExchange().getPrimaryTradeable().getMarketPriceUsd(), RoundingMode.HALF_UP);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,43 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.Setter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
import java.math.BigDecimal;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents an account's balance for a certain amount of tradeables in their
 | 
				
			||||||
 | 
					 * exchange.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
@Entity
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
@Table(name = "account_balance")
 | 
					@Table(name = "account_balance")
 | 
				
			||||||
@Getter
 | 
					@Getter
 | 
				
			||||||
public class Balance {
 | 
					public class Balance {
 | 
				
			||||||
	@Id
 | 
						@EmbeddedId
 | 
				
			||||||
	@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
						private BalanceId balanceId;
 | 
				
			||||||
	private Long id;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@MapsId("accountId")
 | 
				
			||||||
	@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						@JoinColumn(name = "account_id")
 | 
				
			||||||
	private Account account;
 | 
						private Account account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@MapsId("tradeableId")
 | 
				
			||||||
	@ManyToOne(optional = false, fetch = FetchType.EAGER)
 | 
						@ManyToOne(optional = false, fetch = FetchType.EAGER)
 | 
				
			||||||
	private Currency currency;
 | 
						@JoinColumn(name = "tradeable_id")
 | 
				
			||||||
 | 
						private Tradeable tradeable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@Column(nullable = false, precision = 24, scale = 10)
 | 
						@Column(nullable = false, precision = 24, scale = 10)
 | 
				
			||||||
 | 
						@Setter
 | 
				
			||||||
	private BigDecimal amount;
 | 
						private BigDecimal amount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Balance(Account account, Tradeable tradeable, BigDecimal amount) {
 | 
				
			||||||
 | 
							this.balanceId = new BalanceId(tradeable.getId(), account.getId());
 | 
				
			||||||
 | 
							this.account = account;
 | 
				
			||||||
 | 
							this.tradeable = tradeable;
 | 
				
			||||||
 | 
							this.amount = amount;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AllArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.EqualsAndHashCode;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.persistence.Embeddable;
 | 
				
			||||||
 | 
					import java.io.Serializable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Embeddable
 | 
				
			||||||
 | 
					@EqualsAndHashCode
 | 
				
			||||||
 | 
					@AllArgsConstructor
 | 
				
			||||||
 | 
					@NoArgsConstructor
 | 
				
			||||||
 | 
					public class BalanceId implements Serializable {
 | 
				
			||||||
 | 
						private Long tradeableId;
 | 
				
			||||||
 | 
						private Long accountId;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,48 +0,0 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lombok.Getter;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import javax.persistence.*;
 | 
					 | 
				
			||||||
import java.util.Objects;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Represents a type of currency. This can be an actual fiat currency, or
 | 
					 | 
				
			||||||
 * perhaps a cryptocurrency, or stocks, or really anything tradeable on the
 | 
					 | 
				
			||||||
 * exchange.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Entity
 | 
					 | 
				
			||||||
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"identifier", "type"}))
 | 
					 | 
				
			||||||
@Getter
 | 
					 | 
				
			||||||
public class Currency {
 | 
					 | 
				
			||||||
	@Id
 | 
					 | 
				
			||||||
	@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
					 | 
				
			||||||
	private Long id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Column(nullable = false)
 | 
					 | 
				
			||||||
	private String identifier;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Enumerated(EnumType.STRING)
 | 
					 | 
				
			||||||
	private CurrencyType type;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Column(nullable = false)
 | 
					 | 
				
			||||||
	private String name;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Column
 | 
					 | 
				
			||||||
	private String description;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Column(nullable = false)
 | 
					 | 
				
			||||||
	private float minDenomination = 0.01f;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public boolean equals(Object other) {
 | 
					 | 
				
			||||||
		if (!(other instanceof Currency c)) return false;
 | 
					 | 
				
			||||||
		if (c.getId() != null && this.getId() != null) return this.getId().equals(c.getId());
 | 
					 | 
				
			||||||
		return this.identifier.equals(c.getIdentifier()) &&
 | 
					 | 
				
			||||||
				this.type.equals(c.getType());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Override
 | 
					 | 
				
			||||||
	public int hashCode() {
 | 
					 | 
				
			||||||
		return Objects.hash(this.identifier, this.type);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.persistence.Entity;
 | 
				
			||||||
 | 
					import javax.persistence.FetchType;
 | 
				
			||||||
 | 
					import javax.persistence.ManyToOne;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
 | 
					public class CustomTradeable extends Tradeable {
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The exchange that the tradeable belongs to.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Exchange exchange;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,19 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
import java.util.HashSet;
 | 
					import java.util.HashSet;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Represents a large collection of users that interact with each other.
 | 
					 * Represents a large collection of users that interact with each other.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Entity
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
@Getter
 | 
					@Getter
 | 
				
			||||||
public class Exchange {
 | 
					public class Exchange {
 | 
				
			||||||
	@Id
 | 
						@Id
 | 
				
			||||||
| 
						 | 
					@ -22,15 +26,50 @@ public class Exchange {
 | 
				
			||||||
	@Column(nullable = false)
 | 
						@Column(nullable = false)
 | 
				
			||||||
	private String name;
 | 
						private String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@OneToMany(mappedBy = "exchange")
 | 
						/**
 | 
				
			||||||
	private Set<ExchangePair> currencyPairs;
 | 
						 * The primary tradeable that's used by this exchange.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Tradeable primaryTradeable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public Set<Currency> getSupportedCurrencies() {
 | 
						/**
 | 
				
			||||||
		Set<Currency> currencies = new HashSet<>();
 | 
						 * The set of tradeables that this exchange allows users to interact with.
 | 
				
			||||||
		for (var pair : getCurrencyPairs()) {
 | 
						 */
 | 
				
			||||||
			currencies.add(pair.getFromCurrency());
 | 
						@ManyToMany(fetch = FetchType.LAZY)
 | 
				
			||||||
			currencies.add(pair.getToCurrency());
 | 
						@JoinTable(
 | 
				
			||||||
 | 
								name = "exchange_supported_tradeable",
 | 
				
			||||||
 | 
								joinColumns = @JoinColumn(name = "exchange_id"),
 | 
				
			||||||
 | 
								inverseJoinColumns = @JoinColumn(name = "tradeable_id")
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						private Set<Tradeable> supportedTradeables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The set of custom tradeables created specifically for use in this exchange.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
 | 
				
			||||||
 | 
						private Set<CustomTradeable> customTradeables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * The set of accounts that are registered with this exchange.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						@OneToMany(mappedBy = "exchange", fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Set<Account> accounts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Set<Tradeable> getAllTradeables() {
 | 
				
			||||||
 | 
							Set<Tradeable> s = new HashSet<>();
 | 
				
			||||||
 | 
							s.addAll(getSupportedTradeables());
 | 
				
			||||||
 | 
							s.addAll(getCustomTradeables());
 | 
				
			||||||
 | 
							return s;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		return currencies;
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public boolean equals(Object other) {
 | 
				
			||||||
 | 
							if (!(other instanceof Exchange e)) return false;
 | 
				
			||||||
 | 
							return this.getId() != null && e.getId() != null && this.getId().equals(e.getId());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public int hashCode() {
 | 
				
			||||||
 | 
							return Objects.hash(this.getId());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,30 +0,0 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import lombok.Getter;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import javax.persistence.*;
 | 
					 | 
				
			||||||
import java.math.BigDecimal;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Represents a pair of currencies that can be exchanged at a set exchange rate.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Entity
 | 
					 | 
				
			||||||
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"from_currency_id", "to_currency_id", "exchange_id"}))
 | 
					 | 
				
			||||||
@Getter
 | 
					 | 
				
			||||||
public class ExchangePair {
 | 
					 | 
				
			||||||
	@Id
 | 
					 | 
				
			||||||
	@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
					 | 
				
			||||||
	private Long id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@ManyToOne(optional = false)
 | 
					 | 
				
			||||||
	private Exchange exchange;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@ManyToOne(optional = false)
 | 
					 | 
				
			||||||
	private Currency fromCurrency;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@ManyToOne(optional = false)
 | 
					 | 
				
			||||||
	private Currency toCurrency;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Column(nullable = false, precision = 24, scale = 10)
 | 
					 | 
				
			||||||
	private BigDecimal exchangeRate = new BigDecimal("1.0");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.persistence.*;
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents a type of currency. This can be an actual fiat currency, or
 | 
				
			||||||
 | 
					 * perhaps a cryptocurrency, or stocks, or really anything tradeable on the
 | 
				
			||||||
 | 
					 * exchange.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Entity
 | 
				
			||||||
 | 
					@Inheritance(strategy = InheritanceType.JOINED)
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
 | 
					public class Tradeable {
 | 
				
			||||||
 | 
						@Id
 | 
				
			||||||
 | 
						@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
				
			||||||
 | 
						private Long id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private String symbol;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Enumerated(EnumType.STRING)
 | 
				
			||||||
 | 
						private TradeableType type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false, precision = 24, scale = 10)
 | 
				
			||||||
 | 
						private BigDecimal marketPriceUsd = new BigDecimal(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private String name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column
 | 
				
			||||||
 | 
						private String description;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Tradeable(String symbol, TradeableType type, String name, String description, BigDecimal marketPriceUsd) {
 | 
				
			||||||
 | 
							this.symbol = symbol;
 | 
				
			||||||
 | 
							this.type = type;
 | 
				
			||||||
 | 
							this.name = name;
 | 
				
			||||||
 | 
							this.description = description;
 | 
				
			||||||
 | 
							this.marketPriceUsd = marketPriceUsd;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public boolean equals(Object other) {
 | 
				
			||||||
 | 
							if (!(other instanceof Tradeable c)) return false;
 | 
				
			||||||
 | 
							if (this.getId() != null && c.getId() != null) return this.getId().equals(c.getId());
 | 
				
			||||||
 | 
							return this.getSymbol().equals(c.getSymbol()) && this.getType().equals(c.getType());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public int hashCode() {
 | 
				
			||||||
 | 
							return Objects.hash(this.id, this.symbol);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum CurrencyType {
 | 
					public enum TradeableType {
 | 
				
			||||||
	FIAT,
 | 
						FIAT,
 | 
				
			||||||
	CRYPTO,
 | 
						CRYPTO,
 | 
				
			||||||
	STOCK
 | 
						STOCK
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					import org.hibernate.annotations.CreationTimestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javax.persistence.*;
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Represents a permanent record of a trade transaction.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
 | 
					public class Transaction {
 | 
				
			||||||
 | 
						@Id
 | 
				
			||||||
 | 
						@GeneratedValue(strategy = GenerationType.IDENTITY)
 | 
				
			||||||
 | 
						private Long id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false)
 | 
				
			||||||
 | 
						private String accountNumber;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Exchange exchange;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Tradeable from;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false, precision = 24, scale = 10)
 | 
				
			||||||
 | 
						private BigDecimal fromAmount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ManyToOne(optional = false, fetch = FetchType.LAZY)
 | 
				
			||||||
 | 
						private Tradeable to;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false, precision = 24, scale = 10)
 | 
				
			||||||
 | 
						private BigDecimal toAmount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Column(nullable = false, updatable = false)
 | 
				
			||||||
 | 
						private LocalDateTime timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Transaction(String accountNumber, Exchange exchange, Tradeable from, BigDecimal fromAmount, Tradeable to, BigDecimal toAmount, LocalDateTime timestamp) {
 | 
				
			||||||
 | 
							this.accountNumber = accountNumber;
 | 
				
			||||||
 | 
							this.exchange = exchange;
 | 
				
			||||||
 | 
							this.from = from;
 | 
				
			||||||
 | 
							this.fromAmount = fromAmount;
 | 
				
			||||||
 | 
							this.to = to;
 | 
				
			||||||
 | 
							this.toAmount = toAmount;
 | 
				
			||||||
 | 
							this.timestamp = timestamp;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,22 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.model;
 | 
					package nl.andrewl.coyotecredit.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
import org.springframework.security.core.GrantedAuthority;
 | 
					import org.springframework.security.core.GrantedAuthority;
 | 
				
			||||||
import org.springframework.security.core.userdetails.UserDetails;
 | 
					import org.springframework.security.core.userdetails.UserDetails;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.persistence.*;
 | 
					import javax.persistence.*;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.Collections;
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Represents a basic user in the system.
 | 
					 * Represents a basic user in the system.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@Entity
 | 
					@Entity
 | 
				
			||||||
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
@Getter
 | 
					@Getter
 | 
				
			||||||
public class User implements UserDetails {
 | 
					public class User implements UserDetails {
 | 
				
			||||||
	@Id
 | 
						@Id
 | 
				
			||||||
| 
						 | 
					@ -31,6 +35,11 @@ public class User implements UserDetails {
 | 
				
			||||||
	@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
 | 
						@OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
 | 
				
			||||||
	private Set<Account> accounts;
 | 
						private Set<Account> accounts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public User(String username, String passwordHash) {
 | 
				
			||||||
 | 
							this.username = username;
 | 
				
			||||||
 | 
							this.passwordHash = passwordHash;
 | 
				
			||||||
 | 
							this.accounts = new HashSet<>();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// USER DETAILS IMPLEMENTATION
 | 
						// USER DETAILS IMPLEMENTATION
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,22 +1,34 @@
 | 
				
			||||||
package nl.andrewl.coyotecredit.service;
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import lombok.AllArgsConstructor;
 | 
					 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.BalanceData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.ExchangeData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.FullAccountData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.TransactionData;
 | 
				
			||||||
import nl.andrewl.coyotecredit.dao.AccountRepository;
 | 
					import nl.andrewl.coyotecredit.dao.AccountRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.TradeableRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.TransactionRepository;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.Account;
 | 
					import nl.andrewl.coyotecredit.model.Account;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Balance;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Tradeable;
 | 
				
			||||||
import nl.andrewl.coyotecredit.model.User;
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import org.springframework.data.domain.PageRequest;
 | 
				
			||||||
import org.springframework.http.HttpStatus;
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
import org.springframework.stereotype.Service;
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
import org.springframework.transaction.annotation.Transactional;
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.util.MultiValueMap;
 | 
				
			||||||
import org.springframework.web.server.ResponseStatusException;
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.ArrayList;
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Service
 | 
					@Service
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class AccountService {
 | 
					public class AccountService {
 | 
				
			||||||
	private final AccountRepository accountRepository;
 | 
						private final AccountRepository accountRepository;
 | 
				
			||||||
 | 
						private final TransactionRepository transactionRepository;
 | 
				
			||||||
 | 
						private final TradeableRepository tradeableRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static record AccountData (
 | 
						public static record AccountData (
 | 
				
			||||||
			long id,
 | 
								long id,
 | 
				
			||||||
| 
						 | 
					@ -27,40 +39,68 @@ public class AccountService {
 | 
				
			||||||
	@Transactional(readOnly = true)
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
	public List<AccountData> getAccountsOverview(User user) {
 | 
						public List<AccountData> getAccountsOverview(User user) {
 | 
				
			||||||
		return accountRepository.findAllByUser(user).stream()
 | 
							return accountRepository.findAllByUser(user).stream()
 | 
				
			||||||
				.map(a -> new AccountData(a.getId(), a.getNumber(), a.getExchange().getName()))
 | 
									.map(a -> new AccountData(
 | 
				
			||||||
 | 
											a.getId(),
 | 
				
			||||||
 | 
											a.getNumber(),
 | 
				
			||||||
 | 
											a.getExchange().getName()
 | 
				
			||||||
 | 
									))
 | 
				
			||||||
				.toList();
 | 
									.toList();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static class FullAccountData {
 | 
					 | 
				
			||||||
		public long id;
 | 
					 | 
				
			||||||
		public String number;
 | 
					 | 
				
			||||||
		public String exchangeName;
 | 
					 | 
				
			||||||
		public List<BalanceData> balances;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@AllArgsConstructor
 | 
					 | 
				
			||||||
	public static class BalanceData {
 | 
					 | 
				
			||||||
		public String currencyIdentifier;
 | 
					 | 
				
			||||||
		public String amount;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	@Transactional(readOnly = true)
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
	public FullAccountData getAccountData(User user, long accountId) {
 | 
						public FullAccountData getAccountData(User user, long accountId) {
 | 
				
			||||||
		Account account = accountRepository.findById(accountId)
 | 
							Account account = accountRepository.findById(accountId)
 | 
				
			||||||
				.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account userAccount = accountRepository.findByUserAndExchange(user, account.getExchange())
 | 
				
			||||||
		if (!account.getUser().getId().equals(user.getId())) {
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!userAccount.isAdmin() && !account.getUser().getId().equals(user.getId())) {
 | 
				
			||||||
			throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		FullAccountData d = new FullAccountData();
 | 
							List<TransactionData> transactionData = transactionRepository.findAllByAccountNumberOrderByTimestampDesc(account.getNumber(), PageRequest.of(0, 5))
 | 
				
			||||||
		d.id = account.getId();
 | 
									.map(TransactionData::new)
 | 
				
			||||||
		d.number = account.getNumber();
 | 
									.stream().toList();
 | 
				
			||||||
		d.exchangeName = account.getExchange().getName();
 | 
							return new FullAccountData(
 | 
				
			||||||
		List<BalanceData> balanceData = new ArrayList<>();
 | 
									account.getId(),
 | 
				
			||||||
		for (var bal : account.getBalances()) {
 | 
									account.getNumber(),
 | 
				
			||||||
			balanceData.add(new BalanceData(bal.getCurrency().getIdentifier(), bal.getAmount().toPlainString()));
 | 
									account.getName(),
 | 
				
			||||||
 | 
									account.isAdmin(),
 | 
				
			||||||
 | 
									userAccount.isAdmin(),
 | 
				
			||||||
 | 
									new ExchangeData(
 | 
				
			||||||
 | 
											account.getExchange().getId(),
 | 
				
			||||||
 | 
											account.getExchange().getName(),
 | 
				
			||||||
 | 
											account.getExchange().getPrimaryTradeable().getSymbol()
 | 
				
			||||||
 | 
									),
 | 
				
			||||||
 | 
									account.getBalances().stream()
 | 
				
			||||||
 | 
											.map(b -> new BalanceData(b.getTradeable().getId(), b.getTradeable().getSymbol(), b.getAmount().toPlainString()))
 | 
				
			||||||
 | 
											.sorted(Comparator.comparing(BalanceData::symbol))
 | 
				
			||||||
 | 
											.toList(),
 | 
				
			||||||
 | 
									account.getTotalBalance().toPlainString(),
 | 
				
			||||||
 | 
									transactionData
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		d.balances = balanceData;
 | 
					
 | 
				
			||||||
		return d;
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void editBalances(long accountId, User user, MultiValueMap<String, String> paramMap) {
 | 
				
			||||||
 | 
							Account account = accountRepository.findById(accountId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account userAccount = accountRepository.findByUserAndExchange(user, account.getExchange())
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!userAccount.isAdmin()) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							for (var entry : paramMap.entrySet()) {
 | 
				
			||||||
 | 
								if (entry.getKey().startsWith("tradeable-")) {
 | 
				
			||||||
 | 
									long tradeableId = Long.parseLong(entry.getKey().substring(10));
 | 
				
			||||||
 | 
									Tradeable tradeable = tradeableRepository.findById(tradeableId)
 | 
				
			||||||
 | 
											.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
 | 
				
			||||||
 | 
									BigDecimal value = new BigDecimal(entry.getValue().get(0));
 | 
				
			||||||
 | 
									Balance bal = account.getBalanceForTradeable(tradeable);
 | 
				
			||||||
 | 
									if (bal == null) {
 | 
				
			||||||
 | 
										bal = new Balance(account, tradeable, value);
 | 
				
			||||||
 | 
										account.getBalances().add(bal);
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										bal.setAmount(value);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							accountRepository.save(account);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,191 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.*;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.*;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.*;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.util.AccountNumberUtils;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.security.crypto.password.PasswordEncoder;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.math.RoundingMode;
 | 
				
			||||||
 | 
					import java.text.DecimalFormat;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.ZoneOffset;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class ExchangeService {
 | 
				
			||||||
 | 
						private final ExchangeRepository exchangeRepository;
 | 
				
			||||||
 | 
						private final AccountRepository accountRepository;
 | 
				
			||||||
 | 
						private final TransactionRepository transactionRepository;
 | 
				
			||||||
 | 
						private final TradeableRepository tradeableRepository;
 | 
				
			||||||
 | 
						private final UserRepository userRepository;
 | 
				
			||||||
 | 
						private final PasswordEncoder passwordEncoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public FullExchangeData getData(long exchangeId, User user) {
 | 
				
			||||||
 | 
							Exchange e = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, e)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							return new FullExchangeData(
 | 
				
			||||||
 | 
									e.getId(),
 | 
				
			||||||
 | 
									e.getName(),
 | 
				
			||||||
 | 
									new TradeableData(e.getPrimaryTradeable()),
 | 
				
			||||||
 | 
									e.getAllTradeables().stream()
 | 
				
			||||||
 | 
											.map(TradeableData::new)
 | 
				
			||||||
 | 
											.sorted(Comparator.comparing(TradeableData::symbol))
 | 
				
			||||||
 | 
											.toList(),
 | 
				
			||||||
 | 
									account.isAdmin()
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public List<SimpleAccountData> getAccounts(long exchangeId, User user) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, exchange)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!account.isAdmin()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							DecimalFormat df = new DecimalFormat("#,###0.00");
 | 
				
			||||||
 | 
							return exchange.getAccounts().stream()
 | 
				
			||||||
 | 
									.sorted(Comparator.comparing(Account::getName))
 | 
				
			||||||
 | 
									.map(a -> new SimpleAccountData(
 | 
				
			||||||
 | 
											a.getId(),
 | 
				
			||||||
 | 
											a.getNumber(),
 | 
				
			||||||
 | 
											a.getName(),
 | 
				
			||||||
 | 
											a.isAdmin(),
 | 
				
			||||||
 | 
											df.format(a.getTotalBalance()) + ' ' + exchange.getPrimaryTradeable().getSymbol()
 | 
				
			||||||
 | 
									))
 | 
				
			||||||
 | 
									.toList();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public void ensureAdminAccount(long exchangeId, User user) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, exchange)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!account.isAdmin()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public long addAccount(long exchangeId, User user, AddAccountPayload payload) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findByUserAndExchange(user, exchange)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!account.isAdmin()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							User u = userRepository.save(new User(payload.username(), passwordEncoder.encode(payload.password())));
 | 
				
			||||||
 | 
							Account a = accountRepository.save(new Account(
 | 
				
			||||||
 | 
									AccountNumberUtils.generate(),
 | 
				
			||||||
 | 
									u,
 | 
				
			||||||
 | 
									payload.name(),
 | 
				
			||||||
 | 
									exchange
 | 
				
			||||||
 | 
							));
 | 
				
			||||||
 | 
							for (var t : exchange.getAllTradeables()) {
 | 
				
			||||||
 | 
								a.getBalances().add(new Balance(a, t, BigDecimal.ZERO));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							a = accountRepository.save(a);
 | 
				
			||||||
 | 
							return a.getId();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void removeAccount(long exchangeId, long accountId, User user) {
 | 
				
			||||||
 | 
							Exchange exchange = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account account = accountRepository.findById(accountId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Account userAccount = accountRepository.findByUserAndExchange(user, exchange)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							if (!userAccount.isAdmin()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							accountRepository.delete(account);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public Map<Long, String> getCurrentTradeables(long exchangeId) {
 | 
				
			||||||
 | 
							Exchange e = exchangeRepository.findById(exchangeId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							Map<Long, String> tradeables = new HashMap<>();
 | 
				
			||||||
 | 
							for (var t : e.getAllTradeables()) {
 | 
				
			||||||
 | 
								tradeables.put(t.getId(), t.getMarketPriceUsd().toPlainString());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return tradeables;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public void doTrade(long accountId, TradePayload payload, User user) {
 | 
				
			||||||
 | 
							Account account = accountRepository.findById(accountId)
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Account not found."));
 | 
				
			||||||
 | 
							if (!account.getUser().getId().equals(user.getId())) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.NOT_FOUND);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							Exchange exchange = account.getExchange();
 | 
				
			||||||
 | 
							Tradeable from = tradeableRepository.findById(payload.sellTradeableId())
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Sell tradeable not found."));
 | 
				
			||||||
 | 
							Tradeable to = tradeableRepository.findById(payload.buyTradeableId())
 | 
				
			||||||
 | 
									.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Buy tradeable not found."));
 | 
				
			||||||
 | 
							BigDecimal value = new BigDecimal(payload.value());
 | 
				
			||||||
 | 
							if (from.getType().equals(TradeableType.STOCK)) {
 | 
				
			||||||
 | 
								if (!payload.type().equalsIgnoreCase("SELL")) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only perform SELL operations when selling stocks.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (to.getType().equals(TradeableType.STOCK)) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot sell stock to purchase stock.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (to.getType().equals(TradeableType.STOCK)) {
 | 
				
			||||||
 | 
								if (!payload.type().equalsIgnoreCase("BUY")) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only perform BUY operations when buying stocks.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (from.getType().equals(TradeableType.STOCK)) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot sell stock to purchase stock.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (value.compareTo(BigDecimal.ZERO) <= 0) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Only positive value may be specified.");
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							BigDecimal fromValue;
 | 
				
			||||||
 | 
							BigDecimal toValue;
 | 
				
			||||||
 | 
							if (payload.type().equalsIgnoreCase("SELL")) {
 | 
				
			||||||
 | 
								fromValue = value;
 | 
				
			||||||
 | 
								toValue = fromValue.multiply(from.getMarketPriceUsd()).divide(to.getMarketPriceUsd(), RoundingMode.HALF_UP);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								toValue = value;
 | 
				
			||||||
 | 
								fromValue = toValue.multiply(to.getMarketPriceUsd()).divide(from.getMarketPriceUsd(), RoundingMode.HALF_UP);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							Balance fromBalance = account.getBalanceForTradeable(from);
 | 
				
			||||||
 | 
							if (fromBalance == null || fromBalance.getAmount().compareTo(fromValue) < 0) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required balance of " + fromValue.toPlainString() + " from " + from.getSymbol());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							fromBalance.setAmount(fromBalance.getAmount().subtract(fromValue));
 | 
				
			||||||
 | 
							Balance toBalance = account.getBalanceForTradeable(to);
 | 
				
			||||||
 | 
							if (toBalance == null) {
 | 
				
			||||||
 | 
								toBalance = new Balance(account, to, toValue);
 | 
				
			||||||
 | 
								account.getBalances().add(toBalance);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								toBalance.setAmount(toBalance.getAmount().add(toValue));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							accountRepository.save(account);
 | 
				
			||||||
 | 
							Transaction tx = new Transaction(account.getNumber(), exchange, from, fromValue, to, toValue, LocalDateTime.now(ZoneOffset.UTC));
 | 
				
			||||||
 | 
							transactionRepository.save(tx);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.TradeData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.TradeableData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.AccountRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.Account;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.math.BigDecimal;
 | 
				
			||||||
 | 
					import java.util.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class TradeService {
 | 
				
			||||||
 | 
						private final AccountRepository accountRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional(readOnly = true)
 | 
				
			||||||
 | 
						public TradeData getTradeData(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);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							List<TradeableData> sellList = new ArrayList<>();
 | 
				
			||||||
 | 
							Map<String, BigDecimal> accountBalances = new HashMap<>();
 | 
				
			||||||
 | 
							for (var bal : account.getBalances()) {
 | 
				
			||||||
 | 
								sellList.add(new TradeableData(bal.getTradeable()));
 | 
				
			||||||
 | 
								accountBalances.put(bal.getTradeable().getSymbol(), bal.getAmount());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							sellList.sort(Comparator.comparing(TradeableData::symbol));
 | 
				
			||||||
 | 
							List<TradeableData> buyList = new ArrayList<>();
 | 
				
			||||||
 | 
							for (var t : account.getExchange().getAllTradeables()) {
 | 
				
			||||||
 | 
								buyList.add(new TradeableData(t));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return new TradeData(account.getId(), account.getNumber(), account.getExchange().getId(), sellList, accountBalances, buyList);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					package nl.andrewl.coyotecredit.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.ctl.dto.UserData;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.dao.UserRepository;
 | 
				
			||||||
 | 
					import nl.andrewl.coyotecredit.model.User;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class UserService {
 | 
				
			||||||
 | 
						private final UserRepository userRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Transactional
 | 
				
			||||||
 | 
						public UserData getUser(long userId, User requestingUser) {
 | 
				
			||||||
 | 
							User user;
 | 
				
			||||||
 | 
							if (requestingUser.getId().equals(userId)) {
 | 
				
			||||||
 | 
								user = requestingUser;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								user = userRepository.findById(userId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return new UserData(user.getId(), user.getUsername());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ public class AccountNumberUtils {
 | 
				
			||||||
		StringBuilder sb = new StringBuilder(19);
 | 
							StringBuilder sb = new StringBuilder(19);
 | 
				
			||||||
		Random rand = new SecureRandom();
 | 
							Random rand = new SecureRandom();
 | 
				
			||||||
		for (int i = 0; i < 16; i++) {
 | 
							for (int i = 0; i < 16; i++) {
 | 
				
			||||||
			if (i % 4 == 0) sb.append('-');
 | 
								if (i > 0 && i % 4 == 0) sb.append('-');
 | 
				
			||||||
			sb.append(rand.nextInt(0, 10));
 | 
								sb.append(rand.nextInt(0, 10));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return sb.toString();
 | 
							return sb.toString();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,155 @@
 | 
				
			||||||
 | 
					const sellValueInput = document.getElementById("sellValueInput");
 | 
				
			||||||
 | 
					const sellTradeableSelect = document.getElementById("sellTradeableSelect");
 | 
				
			||||||
 | 
					const sellTradeableSelectText = document.getElementById("sellTradeableSelectText");
 | 
				
			||||||
 | 
					const buyValueInput = document.getElementById("buyValueInput");
 | 
				
			||||||
 | 
					const buyTradeableSelect = document.getElementById("buyTradeableSelect");
 | 
				
			||||||
 | 
					const submitButton = document.getElementById("submitButton");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sellTradeableSelect.selectedIndex = null;
 | 
				
			||||||
 | 
					buyTradeableSelect.selectedIndex = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sellTradeableSelect.addEventListener("change", onSellSelectChanged);
 | 
				
			||||||
 | 
					sellValueInput.addEventListener("change", onSellInputChanged);
 | 
				
			||||||
 | 
					buyTradeableSelect.addEventListener("change", onBuySelectChanged);
 | 
				
			||||||
 | 
					buyValueInput.addEventListener("change", onBuyInputChanged);
 | 
				
			||||||
 | 
					submitButton.addEventListener("click", onSubmitClicked);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let tradeables = {};
 | 
				
			||||||
 | 
					const exchangeId = Number(document.getElementById("exchangeIdInput").value);
 | 
				
			||||||
 | 
					const accountId = Number(document.getElementById("accountIdInput").value);
 | 
				
			||||||
 | 
					refreshTradeables();
 | 
				
			||||||
 | 
					window.setInterval(refreshTradeables, 10000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function refreshTradeables() {
 | 
				
			||||||
 | 
					    fetch("/api/exchanges/" + exchangeId + "/tradeables")
 | 
				
			||||||
 | 
					        .then((response) => {
 | 
				
			||||||
 | 
					            if (response.status !== 200) {
 | 
				
			||||||
 | 
					                console.error("Exchange API call failed.");
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                response.json().then((data) => {
 | 
				
			||||||
 | 
					                    tradeables = data;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getSelectedSellOption() {
 | 
				
			||||||
 | 
					    return sellTradeableSelect.options[sellTradeableSelect.selectedIndex];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getSelectedBuyOption() {
 | 
				
			||||||
 | 
					    return buyTradeableSelect.options[buyTradeableSelect.selectedIndex];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function updateInputSettings(input, step, min, max) {
 | 
				
			||||||
 | 
					    input.setAttribute("step", step);
 | 
				
			||||||
 | 
					    input.setAttribute("min", min);
 | 
				
			||||||
 | 
					    input.setAttribute("max", max);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function resetValueInputs() {
 | 
				
			||||||
 | 
					    sellValueInput.value = null;
 | 
				
			||||||
 | 
					    buyValueInput.value = null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Event handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onSellSelectChanged() {
 | 
				
			||||||
 | 
					    resetValueInputs();
 | 
				
			||||||
 | 
					    const sellOption = getSelectedSellOption();
 | 
				
			||||||
 | 
					    const sellType = sellOption.dataset.type;
 | 
				
			||||||
 | 
					    const sellBalance = Number(sellOption.dataset.balance);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Don't allow the user to buy the same thing they're selling.
 | 
				
			||||||
 | 
					    for (let i = 0; i < buyTradeableSelect.length; i++) {
 | 
				
			||||||
 | 
					        const buyOption = buyTradeableSelect.options[i];
 | 
				
			||||||
 | 
					        const buyType = buyOption.dataset.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let isOptionDisabled = buyOption.value === sellOption.value;
 | 
				
			||||||
 | 
					        if (sellType === "STOCK" && buyType === "STOCK") {
 | 
				
			||||||
 | 
					            isOptionDisabled = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isOptionDisabled) {
 | 
				
			||||||
 | 
					            buyOption.setAttribute("disabled", true);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            buyOption.removeAttribute("disabled");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // If the user is currently selecting an invalid choice, reset.
 | 
				
			||||||
 | 
					        if (i === buyTradeableSelect.selectedIndex && isOptionDisabled) {
 | 
				
			||||||
 | 
					            buyTradeableSelect.value = "";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update the sell value input for the current type's parameters.
 | 
				
			||||||
 | 
					    if (sellType === "STOCK") {
 | 
				
			||||||
 | 
					        updateInputSettings(sellValueInput, 1, 0, sellBalance);
 | 
				
			||||||
 | 
					        buyValueInput.setAttribute("readonly", true);
 | 
				
			||||||
 | 
					    } else if (sellType === "FIAT" || sellType === "CRYPTO") {
 | 
				
			||||||
 | 
					        updateInputSettings(sellValueInput, 0.0000000001, 0, sellBalance);
 | 
				
			||||||
 | 
					        buyValueInput.removeAttribute("readonly");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update the subtext to show the value.
 | 
				
			||||||
 | 
					    sellTradeableSelectText.innerText = `Balance: ${sellBalance}`;
 | 
				
			||||||
 | 
					    sellTradeableSelectText.removeAttribute("hidden");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onBuySelectChanged() {
 | 
				
			||||||
 | 
					    resetValueInputs();
 | 
				
			||||||
 | 
					    const buyOption = getSelectedBuyOption();
 | 
				
			||||||
 | 
					    const buyType = buyOption.dataset.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (buyType === "STOCK") {
 | 
				
			||||||
 | 
					        updateInputSettings(buyValueInput, 1, 0, 1000000);
 | 
				
			||||||
 | 
					        sellValueInput.setAttribute("readonly", true);
 | 
				
			||||||
 | 
					    } else if (buyType === "FIAT" || buyType === "CRYPTO") {
 | 
				
			||||||
 | 
					        updateInputSettings(buyValueInput, 0.0000000001, 0, 1000000);
 | 
				
			||||||
 | 
					        sellValueInput.removeAttribute("readonly");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onSellInputChanged() {
 | 
				
			||||||
 | 
					    const sellOption = getSelectedSellOption();
 | 
				
			||||||
 | 
					    const buyOption = getSelectedBuyOption();
 | 
				
			||||||
 | 
					    const sellId = sellOption.value;
 | 
				
			||||||
 | 
					    const buyId = buyOption.value;
 | 
				
			||||||
 | 
					    const sellPriceUsd = tradeables[sellId];
 | 
				
			||||||
 | 
					    const buyPriceUsd = tradeables[buyId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sellPriceUsd && buyPriceUsd) {
 | 
				
			||||||
 | 
					        const sellVolume = Number(sellValueInput.value);
 | 
				
			||||||
 | 
					        buyValueInput.value = (sellPriceUsd * sellVolume) / buyPriceUsd;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onBuyInputChanged() {
 | 
				
			||||||
 | 
					    const sellOption = getSelectedSellOption();
 | 
				
			||||||
 | 
					    const buyOption = getSelectedBuyOption();
 | 
				
			||||||
 | 
					    const sellId = sellOption.value;
 | 
				
			||||||
 | 
					    const buyId = buyOption.value;
 | 
				
			||||||
 | 
					    const sellPriceUsd = tradeables[sellId];
 | 
				
			||||||
 | 
					    const buyPriceUsd = tradeables[buyId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sellPriceUsd && buyPriceUsd) {
 | 
				
			||||||
 | 
					        const buyVolume = Number(buyValueInput.value);
 | 
				
			||||||
 | 
					        sellValueInput.value = (buyPriceUsd * buyVolume) / sellPriceUsd;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onSubmitClicked() {
 | 
				
			||||||
 | 
					    let type = "SELL";
 | 
				
			||||||
 | 
					    let value = Number(sellValueInput.value);
 | 
				
			||||||
 | 
					    if (sellValueInput.hasAttribute("readonly")) {
 | 
				
			||||||
 | 
					        type = "BUY";
 | 
				
			||||||
 | 
					        value = Number(buyValueInput.value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const sellTradeableId = parseInt(getSelectedSellOption().value);
 | 
				
			||||||
 | 
					    const buyTradeableId = parseInt(getSelectedBuyOption().value);
 | 
				
			||||||
 | 
					    const form = document.getElementById("tradeForm");
 | 
				
			||||||
 | 
					    form.elements["type"].value = type;
 | 
				
			||||||
 | 
					    form.elements["sellTradeableId"].value = sellTradeableId;
 | 
				
			||||||
 | 
					    form.elements["buyTradeableId"].value = buyTradeableId;
 | 
				
			||||||
 | 
					    form.elements["value"].value = value;
 | 
				
			||||||
 | 
					    form.submit();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,18 +2,53 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Account', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <title>CC - Account</title>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Account <span th:text="${account.number}"></span></h1>
 | 
					    <h1>Account <span th:text="${account.number()}"></span></h1>
 | 
				
			||||||
 | 
					    <p>In <a th:href="@{/exchanges/{id}(id=${account.exchange().id()})}" th:text="${account.exchange().name()}"></a></p>
 | 
				
			||||||
 | 
					    <p>Total value of <span th:text="${account.totalBalance() + ' ' + account.exchange().primaryTradeable()}"></span></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Balance</h3>
 | 
					    <h3>Overview</h3>
 | 
				
			||||||
    <ul>
 | 
					
 | 
				
			||||||
        <li th:each="balance : ${account.balances}">
 | 
					    <table class="table">
 | 
				
			||||||
            <span th:text="${balance.currencyIdentifier}"></span> - <span th:text="${balance.amount}"></span>
 | 
					        <thead>
 | 
				
			||||||
        </li>
 | 
					            <tr>
 | 
				
			||||||
    </ul>
 | 
					                <th>Currency</th>
 | 
				
			||||||
</body>
 | 
					                <th>Balance</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr th:each="bal : ${account.balances()}">
 | 
				
			||||||
 | 
					                <td th:text="${bal.symbol()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${bal.amount()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h3>Recent Transactions</h3>
 | 
				
			||||||
 | 
					    <table class="table">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th>From</th>
 | 
				
			||||||
 | 
					                <th>Amount From</th>
 | 
				
			||||||
 | 
					                <th>To</th>
 | 
				
			||||||
 | 
					                <th>Amount To</th>
 | 
				
			||||||
 | 
					                <th>Timestamp</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr th:each="tx : ${account.recentTransactions()}">
 | 
				
			||||||
 | 
					                <td th:text="${tx.from().name()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tx.fromAmount()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tx.to().name()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tx.toAmount()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tx.timestamp()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <a class="btn btn-success" th:href="@{/trade/{account}(account=${account.id()})}">Trade</a>
 | 
				
			||||||
 | 
					    <a class="btn btn-primary" th:if="${account.userAdmin()}" th:href="@{/accounts/{aId}/editBalances(aId=${account.id()})}">Edit Balances</a>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,17 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Edit Balances</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form th:action="@{/accounts/{aId}/editBalances(aId=${accountId})}" th:method="post">
 | 
				
			||||||
 | 
					        <div class="mb-3" th:each="bal, iter : ${account.balances()}">
 | 
				
			||||||
 | 
					            <label th:for="${'tradeable-' + bal.id()}" class="form-label" th:text="${bal.symbol()}"></label>
 | 
				
			||||||
 | 
					            <input type="number" min="0" th:value="${bal.amount()}" th:name="${'tradeable-' + bal.id()}" class="form-control"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-success">Submit</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Exchange', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1 th:text="${exchange.name()}"></h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					        Primary tradeable: <span th:text="${exchange.primaryTradeable().name()}"></span>
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <h3>Supported Tradeable Currencies / Stocks</h3>
 | 
				
			||||||
 | 
					    <table class="table">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th>Symbol</th>
 | 
				
			||||||
 | 
					                <th>Type</th>
 | 
				
			||||||
 | 
					                <th>Price ($)</th>
 | 
				
			||||||
 | 
					                <th>Name</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr th:each="tradeable : ${exchange.supportedTradeables()}">
 | 
				
			||||||
 | 
					                <td th:text="${tradeable.symbol()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tradeable.type()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tradeable.marketPriceUsd()}"></td>
 | 
				
			||||||
 | 
					                <td th:text="${tradeable.name()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div th:if="${exchange.accountAdmin()}">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					            <a class="btn btn-primary" th:href="@{/exchanges/{eId}/accounts(eId=${exchange.id()})}">View Accounts</a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Exchange Accounts', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Accounts</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <a class="btn btn-primary" th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}">Add Account</a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <table class="table">
 | 
				
			||||||
 | 
					        <thead>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th>Number</th>
 | 
				
			||||||
 | 
					                <th>Name</th>
 | 
				
			||||||
 | 
					                <th>Admin</th>
 | 
				
			||||||
 | 
					                <th>Balance</th>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </thead>
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr th:each="account : ${accounts}">
 | 
				
			||||||
 | 
					                <td><a 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 th:text="${account.totalBalance()}"></td>
 | 
				
			||||||
 | 
					                <td><a th:href="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${account.id()})}">Remove</a></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Add Account</h1>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					        Use this page to add an account to the exchange.
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form th:href="@{/exchanges/{eId}/addAccount(eId=${exchangeId})}" th:method="post">
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="nameInput" class="form-label">Name</label>
 | 
				
			||||||
 | 
					            <input id="nameInput" type="text" class="form-control" name="name"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="usernameInput" class="form-label">Username</label>
 | 
				
			||||||
 | 
					            <input id="usernameInput" type="text" class="form-control" name="username"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="passwordInput" class="form-label">Password</label>
 | 
				
			||||||
 | 
					            <input id="passwordInput" type="password" class="form-control" name="password"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-primary">Submit</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Add Account', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Remove Account</h1>
 | 
				
			||||||
 | 
					    <p>
 | 
				
			||||||
 | 
					        Are you sure you want to remove this account?
 | 
				
			||||||
 | 
					    </p>
 | 
				
			||||||
 | 
					    <form th:action="@{/exchanges/{eId}/removeAccount/{aId}(eId=${exchangeId}, aId=${accountId})}" th:method="post">
 | 
				
			||||||
 | 
					        <a class="btn btn-secondary" th:href="@{/exchanges/{eId}/accounts(eId=${exchangeId})}">Cancel</a>
 | 
				
			||||||
 | 
					        <button class="btn btn-danger" type="submit">Remove</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <title>header</title>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<nav th:fragment="header" class="navbar navbar-expand-lg navbar-light bg-light">
 | 
				
			||||||
 | 
					    <div class="container-fluid">
 | 
				
			||||||
 | 
					        <a class="navbar-brand" href="/">Coyote Credit</a>
 | 
				
			||||||
 | 
					        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
 | 
				
			||||||
 | 
					            <span class="navbar-toggler-icon"></span>
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					        <div class="collapse navbar-collapse" id="navbarSupportedContent">
 | 
				
			||||||
 | 
					            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
 | 
				
			||||||
 | 
					                <li class="nav-item">
 | 
				
			||||||
 | 
					                    <a class="nav-link" th:href="@{/}">Home</a>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
 | 
					                <li class="nav-item">
 | 
				
			||||||
 | 
					                    <a class="nav-link" th:href="@{/users/{id}(id=${#authentication.getPrincipal().getId()})}">My Profile</a>
 | 
				
			||||||
 | 
					                </li>
 | 
				
			||||||
 | 
					            </ul>
 | 
				
			||||||
 | 
					            <form class="d-flex" th:action="@{/logout}" th:method="post">
 | 
				
			||||||
 | 
					                <button class="btn btn-outline-success" type="submit">Logout</button>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
| 
						 | 
					@ -2,12 +2,10 @@
 | 
				
			||||||
<html
 | 
					<html
 | 
				
			||||||
        lang="en"
 | 
					        lang="en"
 | 
				
			||||||
        xmlns:th="http://www.thymeleaf.org"
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Home', content=~{::#content})}"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
<head>
 | 
					 | 
				
			||||||
    <title>CC - Home</title>
 | 
					 | 
				
			||||||
</head>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<div id="content" class="container">
 | 
				
			||||||
    <h1>Welcome!</h1>
 | 
					    <h1>Welcome!</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <h3>Your Accounts:</h3>
 | 
					    <h3>Your Accounts:</h3>
 | 
				
			||||||
| 
						 | 
					@ -19,8 +17,4 @@
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
        </li>
 | 
					        </li>
 | 
				
			||||||
    </ul>
 | 
					    </ul>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
    <form th:action="@{/logout}" th:method="post">
 | 
					 | 
				
			||||||
        <input type="submit" value="Logout">
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,39 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
 | 
				
			||||||
 | 
					        th:fragment="layout (title, content)"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
 | 
					    <title th:text="${'Coyote Credit - ' + title}">Coyote Credit</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <link rel="stylesheet" href="/static/css/style.css"/>
 | 
				
			||||||
 | 
					    <link
 | 
				
			||||||
 | 
					            href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
 | 
				
			||||||
 | 
					            rel="stylesheet"
 | 
				
			||||||
 | 
					            integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
 | 
				
			||||||
 | 
					            crossorigin="anonymous"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <header sec:authorize="isAuthenticated()">
 | 
				
			||||||
 | 
					        <div th:replace="~{fragment/header :: header}"></div>
 | 
				
			||||||
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="container-fluid">
 | 
				
			||||||
 | 
					        <div th:replace="${content}" class="row">
 | 
				
			||||||
 | 
					            <p>Placeholder content.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script
 | 
				
			||||||
 | 
					            src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
 | 
				
			||||||
 | 
					            integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
 | 
				
			||||||
 | 
					            crossorigin="anonymous"
 | 
				
			||||||
 | 
					    ></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='Trade', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>Trade</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form id="tradeForm" th:action="@{/trade/{account}(account=${data.accountId()})}" method="post">
 | 
				
			||||||
 | 
					        <input type="hidden" id="exchangeIdInput" th:value="${data.exchangeId()}"/>
 | 
				
			||||||
 | 
					        <input type="hidden" id="accountIdInput" th:value="${data.accountId()}"/>
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="sellTradeableSelect" class="form-label">Tradeable to Sell</label>
 | 
				
			||||||
 | 
					            <select id="sellTradeableSelect" class="form-select">
 | 
				
			||||||
 | 
					                <option selected hidden>Choose something to sell</option>
 | 
				
			||||||
 | 
					                <option
 | 
				
			||||||
 | 
					                        th:each="t : ${data.tradeablesToSell()}"
 | 
				
			||||||
 | 
					                        th:value="${t.id()}"
 | 
				
			||||||
 | 
					                        th:text="${t.name() + ' (' + t.symbol() + ')'}"
 | 
				
			||||||
 | 
					                        th:data-priceusd="${t.marketPriceUsd()}"
 | 
				
			||||||
 | 
					                        th:data-type="${t.type()}"
 | 
				
			||||||
 | 
					                        th:data-balance="${data.accountBalances().get(t.symbol()).toPlainString()}"
 | 
				
			||||||
 | 
					                        th:disabled="${data.accountBalances().get(t.symbol()).signum() == 0 ? true : false}"
 | 
				
			||||||
 | 
					                ></option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					            <div id="sellTradeableSelectText" class="form-text" hidden></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="sellValueInput" class="form-label">Value to Sell</label>
 | 
				
			||||||
 | 
					            <input type="number" class="form-control" id="sellValueInput"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="buyTradeableSelect" class="form-label">Tradeable to Buy</label>
 | 
				
			||||||
 | 
					            <select id="buyTradeableSelect" class="form-select">
 | 
				
			||||||
 | 
					                <option value="" selected disabled hidden>Choose something to buy</option>
 | 
				
			||||||
 | 
					                <option
 | 
				
			||||||
 | 
					                        th:each="t : ${data.tradeablesToBuy()}"
 | 
				
			||||||
 | 
					                        th:value="${t.id()}"
 | 
				
			||||||
 | 
					                        th:text="${t.name() + ' (' + t.symbol() + ')'}"
 | 
				
			||||||
 | 
					                        th:data-priceusd="${t.marketPriceUsd()}"
 | 
				
			||||||
 | 
					                        th:data-type="${t.type()}"
 | 
				
			||||||
 | 
					                ></option>
 | 
				
			||||||
 | 
					            </select>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="mb-3">
 | 
				
			||||||
 | 
					            <label for="buyValueInput" class="form-label">Value to Buy</label>
 | 
				
			||||||
 | 
					            <input type="number" class="form-control" id="buyValueInput"/>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <input type="hidden" name="_csrf" th:value="${_csrf.getToken()}"/>
 | 
				
			||||||
 | 
					        <input type="hidden" name="type"/>
 | 
				
			||||||
 | 
					        <input type="hidden" name="sellTradeableId"/>
 | 
				
			||||||
 | 
					        <input type="hidden" name="buyTradeableId"/>
 | 
				
			||||||
 | 
					        <input type="hidden" name="value"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <button id="submitButton" type="button" class="btn btn-primary">Submit</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script src="/static/js/trade.js"></script>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html
 | 
				
			||||||
 | 
					        lang="en"
 | 
				
			||||||
 | 
					        xmlns:th="http://www.thymeleaf.org"
 | 
				
			||||||
 | 
					        th:replace="~{layout/basic_page :: layout (title='My Profile', content=~{::#content})}"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
					<div id="content" class="container">
 | 
				
			||||||
 | 
					    <h1>My Profile</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <table class="table">
 | 
				
			||||||
 | 
					        <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					                <th scope="row">Username</th>
 | 
				
			||||||
 | 
					                <td th:text="${user.username()}"></td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        </tbody>
 | 
				
			||||||
 | 
					    </table>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
		Loading…
	
		Reference in New Issue