Changes between Initial Version and Version 1 of appdevelopment


Ignore:
Timestamp:
03/02/26 00:10:56 (2 weeks ago)
Author:
231020
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • appdevelopment

    v1 v1  
     1= Напреден развој на апликацијата =
     2
     3== Трансакции ==
     4
     5=== Купување на акција (buyStock) ===
     6'''Опис:''' Методот `buyStock` во `PortfolioService` извршува повеќе зависни операции:
     7 * Намалување на балансот на портфолиото за вкупната вредност на купените акции
     8 * Ажурирање на постоечко холдинг (или креирање на нов) со новата количина и пресметана weighted average цена
     9 * Зачувување на трансакцискиот запис
     10
     11Доколку кој било чекор не успее (на пр. акцијата не постои, портфолиото не е пронајдено), целата операција се откажува и балансот останува непроменет.
     12{{{
     13@Transactional
     14    public void buyStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
     15        Portfolio portfolio = portfolioRepository.findById(portfolioId)
     16                .orElseThrow(() -> new RuntimeException("Portfolio not found"));
     17
     18
     19        //multiply for how much stocks are bught
     20        BigDecimal totalCost = pricePerUnit.multiply(BigDecimal.valueOf(quantity));
     21
     22//        if (portfolio.getBalance().compareTo(totalCost) < 0) {
     23//            throw new RuntimeException("not enough balance to buy stock");
     24//        }
     25        //TODO - > BRING BACK , JUST FOR TESTING !!
     26
     27        portfolio.setBalance(portfolio.getBalance().subtract(totalCost));
     28
     29        Stock stock1 = stockRepository.findBySymbol(stockSymbol)
     30                .orElseThrow(() -> new RuntimeException("Stock not found"));
     31
     32        PortfolioHolding holding = holdingRepository
     33                .findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
     34                .orElse(PortfolioHolding.builder()
     35                        .portfolio(portfolio)
     36                        .stock(stock1)
     37                        .quantity(0)
     38                        .avgPrice(BigDecimal.ZERO)
     39                        .build());
     40
     41        // avg price bought
     42        // alkaloid 2 x 22.000 + alkaloid 3x 28.000 average od ova
     43        BigDecimal currentTotalValue = holding.getAvgPrice().multiply(BigDecimal.valueOf(holding.getQuantity()));
     44        BigDecimal newTotalValue = currentTotalValue.add(totalCost);
     45        int newQuantity = holding.getQuantity() + quantity;
     46        BigDecimal newAvgPrice = newTotalValue.divide(BigDecimal.valueOf(newQuantity), BigDecimal.ROUND_HALF_UP);
     47
     48        holding.setQuantity(newQuantity);
     49        holding.setAvgPrice(newAvgPrice);
     50
     51        holdingRepository.save(holding);
     52        portfolioRepository.save(portfolio);
     53
     54
     55        //sava a transaction
     56        Transaction transaction = new Transaction();
     57        transaction.setUser(portfolio.getUser());
     58
     59        Stock stock = stockRepository.findBySymbol(stockSymbol)
     60                .orElseThrow(() -> new RuntimeException("stock not found: " + stockSymbol));
     61        transaction.setStock(stock);
     62/*        transaction.setStock(stockRepository.findBySymbol(stockSymbol)
     63                .orElseThrow(() -> new RuntimeException("Stock not found")));*/
     64        transaction.setType("BUY");
     65        transaction.setQuantity(quantity);
     66        transaction.setPrice(pricePerUnit.doubleValue());
     67        transaction.setTimestamp(LocalDateTime.now());
     68
     69        transactionRepository.save(transaction);
     70    }
     71}}}
     72
     73
     74=== Продавање на акција (sellStock) ===
     75
     76'''Опис:''' Методот `sellStock`:
     77 * Проверка дека корисникот поседува доволно акции
     78 * Намалување на количината во холдингот (или бришење ако достигне 0)
     79 * Зголемување на балансот за приходот од продажбата
     80 * Зачувување на трансакцискиот запис
     81
     82{{{
     83@Transactional
     84    public void sellStock(Long portfolioId, String stockSymbol, int quantity, BigDecimal pricePerUnit) {
     85        Portfolio portfolio = portfolioRepository.findById(portfolioId)
     86                .orElseThrow(() -> new RuntimeException("portfolio not found"));
     87
     88        PortfolioHolding holding = holdingRepository
     89                .findByPortfolioIdAndStock_Symbol(portfolioId, stockSymbol)
     90                .orElseThrow(() -> new RuntimeException("stock not found in portfolio"));
     91
     92
     93        // checks
     94        if (holding.getQuantity() < quantity) {
     95            throw new RuntimeException("not enough shares to sell");
     96        }
     97
     98        if (pricePerUnit == null) {
     99            // fallback: get latest price from stock history or live API
     100            Stock stock = stockRepository.findBySymbol(stockSymbol)
     101                    .orElseThrow(() -> new RuntimeException("stock not found: " + stockSymbol));
     102            pricePerUnit = BigDecimal.valueOf(stock.getCurrentPrice());
     103        }
     104
     105        // gain from the sale made
     106        BigDecimal totalGain = pricePerUnit.multiply(BigDecimal.valueOf(quantity));
     107
     108
     109        holding.setQuantity(holding.getQuantity() - quantity);
     110
     111        // if holding is zero ==== remove it from database
     112        if (holding.getQuantity() == 0) {
     113            holdingRepository.delete(holding);
     114        } else {
     115            holdingRepository.save(holding);
     116        }
     117
     118        // update
     119        portfolio.setBalance(portfolio.getBalance().add(totalGain));
     120        portfolioRepository.save(portfolio);
     121
     122
     123        Stock stock = stockRepository.findBySymbol(stockSymbol)
     124                .orElseThrow(() -> new RuntimeException("stock not found: " + stockSymbol));
     125
     126        Transaction transaction = new Transaction();
     127        transaction.setUser(portfolio.getUser());
     128        transaction.setStock(stock);
     129        transaction.setType("SELL");
     130        transaction.setQuantity(quantity);
     131        transaction.setPrice(pricePerUnit.doubleValue());
     132        transaction.setTimestamp(LocalDateTime.now());
     133
     134        transactionRepository.save(transaction);
     135
     136        System.out.println("saved sell");
     137    }
     138}}}
     139
     140=== Поврзување на Google OAuth акаунт (confirmLink) ===
     141
     142'''Опис:''' Методот `confirmLink` во `AuthLinkController` поврзува Google OAuth акаунт со постоечки интерен акаунт. Операцијата вклучува:
     143 * Верификација и читање на pending OAuth токенот
     144 * Автентикација на интерните credentials на корисникот
     145 * Додавање на `GOOGLE` провајдер во множеството на auth провајдери на корисникот
     146 * Зачувување на корисникот
     147 * Бришење на pending токенот
     148
     149{{{
     150    @PostMapping("/confirm")
     151    @Transactional
     152    public ResponseEntity<?> confirmLink(@RequestBody Map<String, String> body) {
     153        String pendingToken = body.get("pendingToken");
     154        String usernameOrEmail = body.get("username");
     155        String password = body.get("password");
     156
     157        if (pendingToken == null || usernameOrEmail == null || password == null) {
     158            return ResponseEntity.badRequest().body("missing fields");
     159        }
     160
     161        PendingLink pending = pendingLinkRepository.findByToken(pendingToken).orElse(null);
     162        if (pending == null || pending.getExpiresAt().isBefore(Instant.now())) {
     163            return ResponseEntity.status(410).body("pending token invalid or expired");
     164        }
     165
     166        // auth internal credentials
     167        try {
     168            Authentication auth = authenticationManager.authenticate(
     169                    new UsernamePasswordAuthenticationToken(usernameOrEmail, password)
     170            );
     171        } catch (Exception ex) {
     172            return ResponseEntity.status(401).body("Invalid internal credentials");
     173        }
     174
     175        // load user
     176        User user = userRepository.findByUsername(usernameOrEmail)
     177                .orElseGet(() -> userRepository.findByEmail(usernameOrEmail).orElse(null));
     178        if (user == null) {
     179            return ResponseEntity.status(404).body("User not found");
     180        }
     181
     182        // verufy
     183//        if (!user.getEmail().equalsIgnoreCase(pending.getEmail())) {
     184//            return ResponseEntity.status(403).body("Pending link does not match authenticated user");
     185//        }
     186        if (!pending.getUser().getId().equals(user.getId())) {
     187            return ResponseEntity.status(403).body("Pending link does not belong to user");
     188        }
     189
     190
     191
     192        Set<AuthProvider> providers = user.getAuthProviders();
     193        providers.add(AuthProvider.GOOGLE);
     194        user.setAuthProviders(providers);
     195        userRepository.save(user);
     196
     197        // delete pending
     198        pendingLinkRepository.deleteByToken(pendingToken);
     199
     200        // generate token
     201        String jwt = userService.generateToken(user);
     202
     203        Map<String, Object> resp = new HashMap<>();
     204        resp.put("token", jwt);
     205        resp.put("message", "Google account linked and logged in");
     206
     207        return ResponseEntity.ok(resp);
     208    }
     209}}}
     210
     211
     212----
     213
     214== Database Connection Pooling ==
     215
     216Spring Boot автоматски го вклучува **HikariCP** connection pool-от.
     217
     218=== Конфигурација ===
     219
     220Тековната конфигурација во `application.yml` го користи HikariCP со стандардните вредности:
     221
     222{{{
     223# application.yml
     224spring:
     225  datasource:
     226    url: jdbc:postgresql://localhost:9999/db_202526z_va_prj_tradingmk
     227    username: db_202526z_va_prj_tradingmk_owner
     228    password: ••••••••
     229  jpa:
     230    hibernate:
     231      ddl-auto: create-drop
     232    show-sql: true
     233    properties:
     234      hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
     235}}}
     236
     237
     238=== Пример на користење на конекција од pool-от ===
     239
     240Секој повик кон repository методите автоматски зема конекција од HikariCP pool-от и ја враќа по завршување. Следниот пример го прикажува текот при `buyStock`:
     241
     242{{{
     243// При повик на buyStock(), HikariCP управува со конекциите:
     244
     245@Transactional                          // <-- Spring отвора трансакција, HikariCP дава конекција
     246public void buyStock(...) {
     247    portfolioRepository.findById(...)   // конекција од pool-от се користи
     248    stockRepository.findBySymbol(...)   // истата конекција
     249    holdingRepository.save(...)         // истата конекција
     250    portfolioRepository.save(...)       // истата конекција
     251    transactionRepository.save(...)     // истата конекција
     252}                                       // <-- COMMIT, конекцијата се враќа во pool-от
     253}}}
     254
     255Без connection pooling, секој од овие повици би отворал и затворал посебна TCP конекција кон PostgreSQL - операција која трае ~50-100ms. Со HikariCP, конекцијата е веќе отворена и чека во pool-от.