wiki:appdevelopment

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

--

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

Трансакции

Купување на акција (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"));


        //multiply for how much stocks are bught
        BigDecimal totalCost = pricePerUnit.multiply(BigDecimal.valueOf(quantity));

//        if (portfolio.getBalance().compareTo(totalCost) < 0) {
//            throw new RuntimeException("not enough balance to buy stock");
//        }
        //TODO - > BRING BACK , JUST FOR TESTING !!

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

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

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

        // avg price bought
        // alkaloid 2 x 22.000 + alkaloid 3x 28.000 average od ova
        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), BigDecimal.ROUND_HALF_UP);

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

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


        //sava a transaction
        Transaction transaction = new Transaction();
        transaction.setUser(portfolio.getUser());

        Stock stock = stockRepository.findBySymbol(stockSymbol)
                .orElseThrow(() -> new RuntimeException("stock not found: " + stockSymbol));
        transaction.setStock(stock);
/*        transaction.setStock(stockRepository.findBySymbol(stockSymbol)
                .orElseThrow(() -> new RuntimeException("Stock not found")));*/
        transaction.setType("BUY");
        transaction.setQuantity(quantity);
        transaction.setPrice(pricePerUnit.doubleValue());
        transaction.setTimestamp(LocalDateTime.now());

        transactionRepository.save(transaction);
    }

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

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

  • Проверка дека корисникот поседува доволно акции
  • Намалување на количината во холдингот (или бришење ако достигне 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"));


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

        if (pricePerUnit == null) {
            // fallback: get latest price from stock history or live API
            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 is zero ==== remove it from database
        if (holding.getQuantity() == 0) {
            holdingRepository.delete(holding);
        } else {
            holdingRepository.save(holding);
        }

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


        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);

        System.out.println("saved sell");
    }

Опис: Методот 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.