wiki:appdevelopment

Version 2 (modified by 231020, 2 weeks ago) ( diff )

--

Напреден развој на апликацијата

Трансакции

executeTrade (точка за влез)

Опис: Методот executeTrade во PortfolioService е централната точка за влез која ја повикува TradeRequestController при одобрување на барање за тргување. Врши рутирање кон buyStock или sellStock врз основа на типот на трансакцијата.

Целата операција е @Transactional - доколку кој било чекор не успее, се откажува целосно.

@Transactional
public void executeTrade(TradeRequest tr) {
    BigDecimal price = BigDecimal.valueOf(tr.getPricePerUnit());
    Portfolio portfolio = tr.getPortfolio();

    if ("BUY".equalsIgnoreCase(tr.getType())) {
        buyStock(portfolio.getId(), tr.getStockSymbol(), tr.getQuantity(), price);
    } else if ("SELL".equalsIgnoreCase(tr.getType())) {
        sellStock(portfolio.getId(), tr.getStockSymbol(), tr.getQuantity(), price);
    } else {
        throw new RuntimeException("Unknown trade type: " + tr.getType());
    }
}

Купување на акција (buyStock)

Опис: Методот buyStock во PortfolioService извршува повеќе зависни операции:

  • Проверка дека портфолиото има доволно баланс за купување
  • Намалување на балансот на портфолиото за вкупната вредност на купените акции
  • Ажурирање на постоечко холдинг (или креирање на нов) со новата количина и пресметана weighted average цена
  • Зачувување на трансакцискиот запис

Доколку кој било чекор не успее (на пр. акцијата не постои, портфолиото не е пронајдено, недоволен баланс), целата операција се откажува и балансот останува непроменет.

@Transactional
public void buyStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
    Portfolio portfolio = portfolioRepository.findById(portfolioId)
            .orElseThrow(() -> new RuntimeException("Portfolio not found"));

    BigDecimal totalCost = pricePerUnit.multiply(BigDecimal.valueOf(quantity));

    if (portfolio.getBalance().compareTo(totalCost) < 0) {
        throw new RuntimeException("Insufficient balance to buy stock");
    }

    portfolio.setBalance(portfolio.getBalance().subtract(totalCost));

    Stock stock = stockRepository.findBySymbol(stockSymbol)
            .orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));

    PortfolioHolding holding = holdingRepository
            .findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
            .orElse(PortfolioHolding.builder()
                    .portfolio(portfolio)
                    .stock(stock)
                    .quantity(0)
                    .avgPrice(BigDecimal.ZERO)
                    .build());

    // Recalculate weighted average price
    BigDecimal currentTotalValue = holding.getAvgPrice().multiply(BigDecimal.valueOf(holding.getQuantity()));
    BigDecimal newTotalValue = currentTotalValue.add(totalCost);
    int newQuantity = holding.getQuantity() + quantity;
    BigDecimal newAvgPrice = newTotalValue.divide(BigDecimal.valueOf(newQuantity), RoundingMode.HALF_UP);

    holding.setQuantity(newQuantity);
    holding.setAvgPrice(newAvgPrice);

    holdingRepository.save(holding);
    portfolioRepository.save(portfolio);

    // Save transaction
    Transaction transaction = new Transaction();
    transaction.setUser(portfolio.getUser());
    transaction.setStock(stock);
    transaction.setType("BUY");
    transaction.setQuantity(quantity);
    transaction.setPrice(pricePerUnit.doubleValue());
    transaction.setTimestamp(LocalDateTime.now());
    transactionRepository.save(transaction);
}

Продавање на акција (sellStock)

Опис: Методот sellStock во PortfolioService:

  • Проверка дека корисникот поседува доволно акции
  • Намалување на количината во холдингот (или бришење ако достигне 0)
  • Зголемување на балансот за приходот од продажбата
  • Зачувување на трансакцискиот запис
@Transactional
public void sellStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
    Portfolio portfolio = portfolioRepository.findById(portfolioId)
            .orElseThrow(() -> new RuntimeException("Portfolio not found"));

    PortfolioHolding holding = holdingRepository
            .findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
            .orElseThrow(() -> new RuntimeException("Stock not found in portfolio"));

    if (holding.getQuantity() < quantity) {
        throw new RuntimeException("Not enough shares to sell");
    }

    if (pricePerUnit == null) {
        // fallback: get latest price from stock
        Stock stock = stockRepository.findBySymbol(stockSymbol)
                .orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));
        pricePerUnit = BigDecimal.valueOf(stock.getCurrentPrice());
    }

    // gain from the sale made
    BigDecimal totalGain = pricePerUnit.multiply(BigDecimal.valueOf(quantity));

    holding.setQuantity(holding.getQuantity() - quantity);

    // if holding reaches zero - remove it from database
    if (holding.getQuantity() == 0) {
        holdingRepository.delete(holding);
    } else {
        holdingRepository.save(holding);
    }

    portfolio.setBalance(portfolio.getBalance().add(totalGain));
    portfolioRepository.save(portfolio);

    // Save transaction
    Stock stock = stockRepository.findBySymbol(stockSymbol)
            .orElseThrow(() -> new RuntimeException("Stock not found: " + stockSymbol));

    Transaction transaction = new Transaction();
    transaction.setUser(portfolio.getUser());
    transaction.setStock(stock);
    transaction.setType("SELL");
    transaction.setQuantity(quantity);
    transaction.setPrice(pricePerUnit.doubleValue());
    transaction.setTimestamp(LocalDateTime.now());
    transactionRepository.save(transaction);
}

Опис: Методот confirmLink во AuthLinkController поврзува Google OAuth акаунт со постоечки интерен акаунт. Операцијата вклучува:

  • Верификација и читање на pending OAuth токенот
  • Автентикација на интерните credentials на корисникот
  • Додавање на GOOGLE провајдер во множеството на auth провајдери на корисникот
  • Зачувување на корисникот
  • Бришење на pending токенот
    @PostMapping("/confirm")
    @Transactional 
    public ResponseEntity<?> confirmLink(@RequestBody Map<String, String> body) {
        String pendingToken = body.get("pendingToken");
        String usernameOrEmail = body.get("username");
        String password = body.get("password");

        if (pendingToken == null || usernameOrEmail == null || password == null) {
            return ResponseEntity.badRequest().body("missing fields");
        }

        PendingLink pending = pendingLinkRepository.findByToken(pendingToken).orElse(null);
        if (pending == null || pending.getExpiresAt().isBefore(Instant.now())) {
            return ResponseEntity.status(410).body("pending token invalid or expired");
        }

        // auth internal credentials
        try {
            Authentication auth = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(usernameOrEmail, password)
            );
        } catch (Exception ex) {
            return ResponseEntity.status(401).body("Invalid internal credentials");
        }

        // load user
        User user = userRepository.findByUsername(usernameOrEmail)
                .orElseGet(() -> userRepository.findByEmail(usernameOrEmail).orElse(null));
        if (user == null) {
            return ResponseEntity.status(404).body("User not found");
        }

        // verufy
//        if (!user.getEmail().equalsIgnoreCase(pending.getEmail())) {
//            return ResponseEntity.status(403).body("Pending link does not match authenticated user");
//        }
        if (!pending.getUser().getId().equals(user.getId())) {
            return ResponseEntity.status(403).body("Pending link does not belong to user");
        }



        Set<AuthProvider> providers = user.getAuthProviders();
        providers.add(AuthProvider.GOOGLE);
        user.setAuthProviders(providers);
        userRepository.save(user);

        // delete pending
        pendingLinkRepository.deleteByToken(pendingToken);

        // generate token
        String jwt = userService.generateToken(user);

        Map<String, Object> resp = new HashMap<>();
        resp.put("token", jwt);
        resp.put("message", "Google account linked and logged in");

        return ResponseEntity.ok(resp);
    }

Database Connection Pooling

Spring Boot автоматски го вклучува HikariCP connection pool-от.

Конфигурација

Тековната конфигурација во application.yml го користи HikariCP со стандардните вредности:

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:9999/db_202526z_va_prj_tradingmk
    username: db_202526z_va_prj_tradingmk_owner
    password: ••••••••
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect

Пример на користење на конекција од pool-от

Секој повик кон repository методите автоматски зема конекција од HikariCP pool-от и ја враќа по завршување. Следниот пример го прикажува текот при buyStock:

// При повик на buyStock(), HikariCP управува со конекциите:

@Transactional                          // <-- Spring отвора трансакција, HikariCP дава конекција
public void buyStock(...) {
    portfolioRepository.findById(...)   // конекција од pool-от се користи
    stockRepository.findBySymbol(...)   // истата конекција 
    holdingRepository.save(...)         // истата конекција
    portfolioRepository.save(...)       // истата конекција
    transactionRepository.save(...)     // истата конекција
}                                       // <-- COMMIT, конекцијата се враќа во pool-от

Без connection pooling, секој од овие повици би отворал и затворал посебна TCP конекција кон PostgreSQL - операција која трае ~50-100ms. Со HikariCP, конекцијата е веќе отворена и чека во pool-от.

Note: See TracWiki for help on using the wiki.