Repository: RAF-SI-2024/Banka-3-Backend
Pull request #213: Implementirana kompletna funkcionalnost za klijentske OTC ponude sa D…
Author: login: Mihailo13222rn
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
name: size/XL
description: Denotes a PR that changes 500-999 lines, ignoring generated files.
Created: 2025-04-06T05:18:48Z | Merged: 2025-04-08T21:36:26Z

…TO-em, mapperom, testovima i validiranim endpointima.

## Opis
Implementirana kompletna OTC funkcionalnost za klijente, uključujući kreiranje, pregled, prihvatanje, odbijanje i kontra-ponude. 
Kreiranje OTC ponude (POST /api/otc)

Pregled svih ponuda koje je korisnik primio (GET /api/otc/received)

Prihvatanje ponude (PUT /api/otc/{id}/accept)

Odbijanje ponude (PUT /api/otc/{id}/reject)

Slanje kontra ponude (PUT /api/otc/{id}/counter)

## Screenshot-ovi (ako je primenjivo)
![image](https://github.com/user-attachments/assets/9bce4015-0323-4919-bd9a-6f4ff6b173ff)

![image](https://github.com/user-attachments/assets/b94c6fc0-1324-4177-af42-24f28d129088)

![image](https://github.com/user-attachments/assets/d354212f-8c58-495a-8bae-8f56d1daac5c)

![image](https://github.com/user-attachments/assets/e244d85f-f640-470d-a79f-1ed4449e630b)

![image](https://github.com/user-attachments/assets/ec70969a-18ca-4841-8684-80a01c4723ec)

![image](https://github.com/user-attachments/assets/070416c2-dc7b-48b1-ad25-41ecc1ad48ab)


## Objašnjenje korisničkog toka (ako je primenjivo)
Klijent kreira OTC ponudu za javno dostupne akcije koje neko drugi poseduje (POST /api/otc).

Prodavac vidi sve primljene ponude u GET /api/otc/received.

Prodavac može da:

prihvati ponudu preko PUT /api/otc/{id}/accept

odbije ponudu preko PUT /api/otc/{id}/reject

pošalje kontra-ponudu putem PUT /api/otc/{id}/counter, gde unosi nove uslove

Svi odgovori su validirani i vraćaju korisnički čitljive poruke ili DTO objekte.

## Dodatne napomene

Mapper koristi StockMapper za ugnježdene entitete u DTO-ima.


Diff:
diff --git a/stock-service/src/main/java/rs/raf/stock_service/StockServiceApplication.java b/stock-service/src/main/java/rs/raf/stock_service/StockServiceApplication.java
index ec663b5b..7cea081f 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/StockServiceApplication.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/StockServiceApplication.java
@@ -15,10 +15,10 @@ public static void main(String[] args) {
         if (System.getenv("ALPHAVANTAGE_API_KEY") == null) {
             Dotenv dotenv = Dotenv.load();
             System.setProperty("ALPHAVANTAGE_API_KEY", dotenv.get("ALPHAVANTAGE_API_KEY"));
-            System.out.println("Loaded AlphaVantage API Key from env: " + dotenv.get("ALPHAVANTAGE_API_KEY"));
+            System.out.println("Loaded AlphaVantage API Key from ...env: " + dotenv.get("ALPHAVANTAGE_API_KEY"));
 
         } else {
-            System.out.println("Loaded AlphaVantage API Key from env (docker): " + System.getenv("ALPHAVANTAGE_API_KEY"));
+            System.out.println("Loaded AlphaVantage API Key from ...env (docker): " + System.getenv("ALPHAVANTAGE_API_KEY"));
         }
         SpringApplication.run(StockServiceApplication.class, args);
     }
diff --git a/stock-service/src/main/java/rs/raf/stock_service/client/UserClient.java b/stock-service/src/main/java/rs/raf/stock_service/client/UserClient.java
index 8ecfbabf..f214b7b2 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/client/UserClient.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/client/UserClient.java
@@ -3,6 +3,7 @@
 import org.springframework.cloud.openfeign.FeignClient;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
+import rs.raf.stock_service.domain.dto.ActuaryDto;
 import rs.raf.stock_service.domain.dto.ActuaryLimitDto;
 import rs.raf.stock_service.domain.dto.ClientDto;
 
@@ -17,6 +18,9 @@ public interface UserClient {
     @GetMapping("/api/admin/clients/{id}")
     ClientDto getClientById(@PathVariable("id") Long id);
 
+    @GetMapping("/api/admin/employees/{id}")
+    ActuaryDto getEmployeeById(@PathVariable("id") Long id);
+
 
 }
 
diff --git a/stock-service/src/main/java/rs/raf/stock_service/controller/OtcOfferController.java b/stock-service/src/main/java/rs/raf/stock_service/controller/OtcOfferController.java
new file mode 100644
index 00000000..b325a95c
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/controller/OtcOfferController.java
@@ -0,0 +1,141 @@
+package rs.raf.stock_service.controller;
+
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import rs.raf.stock_service.domain.dto.CreateOtcOfferDto;
+import rs.raf.stock_service.domain.dto.OtcOfferDto;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.service.OtcService;
+import rs.raf.stock_service.utils.JwtTokenUtil;
+
+import javax.persistence.EntityNotFoundException;
+import javax.validation.Valid;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+@RestController
+@RequestMapping("/api/otc")
+@RequiredArgsConstructor
+public class OtcOfferController {
+
+    private final OtcService otcService;
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Operation(summary = "Create OTC offer", description = "Allows CLIENT or AGENT to create an OTC offer for a public stock.")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "201", description = "OTC offer created successfully."),
+            @ApiResponse(responseCode = "400", description = "Invalid input or insufficient shares."),
+            @ApiResponse(responseCode = "403", description = "Access denied."),
+            @ApiResponse(responseCode = "404", description = "Stock or seller not found."),
+            @ApiResponse(responseCode = "500", description = "Unexpected server error.")
+    })
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    @PostMapping
+    public ResponseEntity<?> createOtcOffer(@RequestHeader("Authorization") String authHeader,
+                                            @Valid @RequestBody CreateOtcOfferDto dto) {
+        try {
+            Long buyerId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            OtcOfferDto created = otcService.createOffer(dto, buyerId);
+            return ResponseEntity.status(HttpStatus.CREATED).body(created);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        } catch (EntityNotFoundException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "Get all active OTC offers received by the user", description = "Returns pending offers where the user is the seller.")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "Offers retrieved."),
+            @ApiResponse(responseCode = "204", description = "No active offers."),
+            @ApiResponse(responseCode = "403", description = "Access denied."),
+            @ApiResponse(responseCode = "500", description = "Unexpected server error.")
+    })
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    @GetMapping("/received")
+    public ResponseEntity<?> getReceivedOffers(@RequestHeader("Authorization") String authHeader) {
+        try {
+            Long userId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            List<OtcOfferDto> offers = otcService.getAllActiveOffersForUser(userId);
+            return ResponseEntity.ok(offers);
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @PutMapping("/{id}/accept")
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    public ResponseEntity<?> acceptOffer(@RequestHeader("Authorization") String authHeader,
+                                         @PathVariable Long id) {
+        try {
+            Long sellerId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            otcService.acceptOffer(id, sellerId);
+            return ResponseEntity.ok("Offer successfully accepted.");
+        } catch (NoSuchElementException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Offer not found");
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @PutMapping("/{id}/reject")
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    public ResponseEntity<?> rejectOffer(@RequestHeader("Authorization") String authHeader,
+                                         @PathVariable Long id) {
+        try {
+            Long sellerId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            otcService.rejectOffer(id, sellerId);
+            return ResponseEntity.ok("Offer successfully rejected.");
+        } catch (NoSuchElementException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Offer not found");
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @PutMapping("/{id}/counter")
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    public ResponseEntity<?> counterOffer(@RequestHeader("Authorization") String authHeader,
+                                          @PathVariable Long id,
+                                          @Valid @RequestBody CreateOtcOfferDto dto) {
+        try {
+            Long sellerId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            otcService.updateOffer(id, sellerId, dto);
+            return ResponseEntity.ok("Counter-offer sent.");
+        } catch (NoSuchElementException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Offer not found");
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @PutMapping("/{id}/cancel")
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    @Operation(summary = "Cancel OTC offer")
+    @ApiResponses({
+            @ApiResponse(responseCode = "200", description = "Offer successfully cancelled"),
+            @ApiResponse(responseCode = "403", description = "Unauthorized to cancel"),
+            @ApiResponse(responseCode = "404", description = "Offer not found")
+    })
+    public ResponseEntity<?> cancelOffer(@PathVariable("id") Long offerId,
+                                         @RequestHeader("Authorization") String authHeader) {
+        try {
+            Long userId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            otcService.cancelOffer(offerId, userId);
+            return ResponseEntity.ok("Offer successfully cancelled.");
+        }  catch (NoSuchElementException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Offer not found");
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/dto/ActuaryDto.java b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/ActuaryDto.java
new file mode 100644
index 00000000..09ec4377
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/ActuaryDto.java
@@ -0,0 +1,14 @@
+package rs.raf.stock_service.domain.dto;
+
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@Getter
+@Setter
+@AllArgsConstructor
+@Builder
+public class ActuaryDto {
+    private String firstName;
+    private String lastName;
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/dto/CreateOtcOfferDto.java b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/CreateOtcOfferDto.java
new file mode 100644
index 00000000..5fd70d54
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/CreateOtcOfferDto.java
@@ -0,0 +1,38 @@
+package rs.raf.stock_service.domain.dto;
+
+import lombok.*;
+
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.Future;
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Getter
+@Setter
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateOtcOfferDto {
+
+    @NotNull
+    private Long portfolioEntryId;
+
+    @NotNull
+    @DecimalMin("0.01")
+    private BigDecimal amount;
+
+    @NotNull
+    @DecimalMin("0.01")
+    private BigDecimal pricePerStock; // koristiš ovo umesto strikePrice
+
+    @NotNull
+    @DecimalMin("0.00")
+    private BigDecimal premium;
+
+    @NotNull
+    @Future
+    private LocalDate settlementDate;
+
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/dto/OtcOfferDto.java b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/OtcOfferDto.java
new file mode 100644
index 00000000..e1cd9898
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/OtcOfferDto.java
@@ -0,0 +1,29 @@
+package rs.raf.stock_service.domain.dto;
+
+import lombok.*;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OtcOfferDto {
+
+    private Long id;
+    private StockDto stock;
+
+    private Integer amount;
+    private BigDecimal pricePerStock;
+    private BigDecimal premium;
+    private LocalDate settlementDate;
+
+    private OtcOfferStatus status;
+
+    private Boolean canInteract; // true ako je lastModifiedById različit od userId pozivaoca
+    private String name;
+
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/dto/StockDto.java b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/StockDto.java
index 32fea2e2..67f301d4 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/domain/dto/StockDto.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/dto/StockDto.java
@@ -1,9 +1,14 @@
 package rs.raf.stock_service.domain.dto;
 
-import lombok.Data;
+import lombok.*;
 
 import java.math.BigDecimal;
 
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
 @Data
 public class StockDto {
     private String name;
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/enums/OtcOfferStatus.java b/stock-service/src/main/java/rs/raf/stock_service/domain/enums/OtcOfferStatus.java
index 7373591e..0cefc0d0 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/domain/enums/OtcOfferStatus.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/enums/OtcOfferStatus.java
@@ -3,5 +3,6 @@
 public enum OtcOfferStatus {
     PENDING,
     ACCEPTED,
-    REJECTED
+    REJECTED,
+    CANCELLED
 }
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/OtcOfferMapper.java b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/OtcOfferMapper.java
new file mode 100644
index 00000000..bfbd4e46
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/OtcOfferMapper.java
@@ -0,0 +1,33 @@
+package rs.raf.stock_service.domain.mapper;
+
+import lombok.*;
+import org.springframework.stereotype.Component;
+import rs.raf.stock_service.client.UserClient;
+import rs.raf.stock_service.domain.dto.OtcOfferDto;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.domain.mapper.StockOptionMapper;
+
+
+@Component
+@AllArgsConstructor
+public class OtcOfferMapper {
+
+    private final StockMapper stockMapper;
+
+
+    public OtcOfferDto toDto(OtcOffer offer, Long userId) {
+        boolean canInteract = !offer.getLastModifiedById().equals(userId);
+
+        return OtcOfferDto.builder()
+                .id(offer.getId())
+                .stock(stockMapper.toDto(offer.getStock()))
+                .amount(offer.getAmount())
+                .pricePerStock(offer.getPricePerStock())
+                .premium(offer.getPremium())
+                .settlementDate(offer.getSettlementDate())
+                .status(offer.getStatus())
+                .canInteract(canInteract)
+                .build();
+    }
+}
+
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockMapper.java b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockMapper.java
new file mode 100644
index 00000000..57f3d1ba
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockMapper.java
@@ -0,0 +1,24 @@
+package rs.raf.stock_service.domain.mapper;
+
+import org.springframework.stereotype.Component;
+import rs.raf.stock_service.domain.dto.StockDto;
+import rs.raf.stock_service.domain.entity.Stock;
+
+@Component
+public class StockMapper {
+
+    public StockDto toDto(Stock stock) {
+        return StockDto.builder()
+                .name(stock.getName())
+                .ticker(stock.getTicker())
+                .price(stock.getPrice())
+                .change(stock.getChange())
+                .volume(stock.getVolume())
+                .outstandingShares(stock.getOutstandingShares())
+                .dividendYield(stock.getDividendYield())
+                .marketCap(stock.getMarketCap())
+                .maintenanceMargin(stock.getMaintenanceMargin())
+                .exchange(stock.getExchange().getName()) // Ako je `exchange` entitet
+                .build();
+    }
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockOptionMapper.java b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockOptionMapper.java
index 09122782..354caa91 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockOptionMapper.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/domain/mapper/StockOptionMapper.java
@@ -1,8 +1,10 @@
 package rs.raf.stock_service.domain.mapper;
 
 import org.springframework.stereotype.Component;
+import rs.raf.stock_service.domain.dto.StockDto;
 import rs.raf.stock_service.domain.dto.StockOptionDto;
 import rs.raf.stock_service.domain.entity.Option;
+import rs.raf.stock_service.domain.entity.Stock;
 
 @Component
 public class StockOptionMapper {
diff --git a/stock-service/src/main/java/rs/raf/stock_service/exceptions/UnauthorizedActionException.java b/stock-service/src/main/java/rs/raf/stock_service/exceptions/UnauthorizedActionException.java
new file mode 100644
index 00000000..cc06267d
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/exceptions/UnauthorizedActionException.java
@@ -0,0 +1,7 @@
+package rs.raf.stock_service.exceptions;
+
+public class UnauthorizedActionException extends RuntimeException{
+    public UnauthorizedActionException(String message){
+        super(message);
+    }
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/repository/OtcOfferRepository.java b/stock-service/src/main/java/rs/raf/stock_service/repository/OtcOfferRepository.java
new file mode 100644
index 00000000..576ba3e2
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/repository/OtcOfferRepository.java
@@ -0,0 +1,18 @@
+package rs.raf.stock_service.repository;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import rs.raf.stock_service.domain.entity.Listing;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.domain.entity.PortfolioEntry;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public interface OtcOfferRepository extends JpaRepository<OtcOffer, Long> {
+
+
+    List<OtcOffer> findAllByStatus(OtcOfferStatus status);
+}
diff --git a/stock-service/src/main/java/rs/raf/stock_service/service/OtcService.java b/stock-service/src/main/java/rs/raf/stock_service/service/OtcService.java
new file mode 100644
index 00000000..d9ad2c28
--- /dev/null
+++ b/stock-service/src/main/java/rs/raf/stock_service/service/OtcService.java
@@ -0,0 +1,175 @@
+package rs.raf.stock_service.service;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import rs.raf.stock_service.client.UserClient;
+import rs.raf.stock_service.domain.dto.*;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.domain.entity.PortfolioEntry;
+import rs.raf.stock_service.domain.entity.Stock;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+import rs.raf.stock_service.domain.mapper.OtcOfferMapper;
+import rs.raf.stock_service.exceptions.InvalidPublicAmountException;
+import rs.raf.stock_service.exceptions.PortfolioEntryNotFoundException;
+import rs.raf.stock_service.exceptions.UnauthorizedActionException;
+import rs.raf.stock_service.repository.OtcOfferRepository;
+import rs.raf.stock_service.repository.PortfolioEntryRepository;
+
+import javax.persistence.EntityNotFoundException;
+import javax.transaction.Transactional;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class OtcService {
+
+    private final OtcOfferRepository otcOfferRepository;
+    private final PortfolioEntryRepository portfolioEntryRepository;
+    private final OtcOfferMapper otcOfferMapper;
+    private final UserClient userClient;
+
+    public OtcOfferDto createOffer(CreateOtcOfferDto dto, Long buyerId) {
+        PortfolioEntry sellerEntry = portfolioEntryRepository.findById(dto.getPortfolioEntryId())
+                .orElseThrow(PortfolioEntryNotFoundException::new);
+
+        Stock stock = (Stock) sellerEntry.getListing();
+        Long sellerId = sellerEntry.getUserId();
+
+        if (dto.getAmount().compareTo(BigDecimal.valueOf(sellerEntry.getPublicAmount())) > 0) {
+            throw new InvalidPublicAmountException("Not enough public shares to fulfill the offer");
+        }
+
+        OtcOffer offer = OtcOffer.builder()
+                .stock(stock)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .amount(dto.getAmount().intValue())
+                .pricePerStock(dto.getPricePerStock())
+                .premium(dto.getPremium())
+                .settlementDate(dto.getSettlementDate())
+                .status(OtcOfferStatus.PENDING)
+                .lastModified(LocalDateTime.now())
+                .lastModifiedById(buyerId)
+                .build();
+
+        return otcOfferMapper.toDto(otcOfferRepository.save(offer), buyerId);
+    }
+
+    public List<OtcOfferDto> getAllActiveOffersForUser(Long userId) {
+        return otcOfferRepository.findAllByStatus(OtcOfferStatus.PENDING).stream()
+                .filter(offer -> offer.getSellerId().equals(userId) || offer.getBuyerId().equals(userId))
+                .sorted(Comparator.comparing(OtcOffer::getLastModified).reversed())
+                .map(offer -> {
+                    OtcOfferDto dto = otcOfferMapper.toDto(offer, userId);
+
+                    boolean canInteract = !offer.getLastModifiedById().equals(userId);
+                    dto.setCanInteract(canInteract);
+
+                    Long nameUserId;
+                    if (canInteract) {
+                        nameUserId = offer.getLastModifiedById(); // Onaj koji je poslednji slao
+                    } else {
+                        nameUserId = userId.equals(offer.getBuyerId()) ? offer.getSellerId() : offer.getBuyerId(); // druga strana
+                    }
+
+                    dto.setName(resolveUserName(nameUserId));
+                    return dto;
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Transactional
+    public void acceptOffer(Long offerId, Long userId) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!(userId.equals(offer.getSellerId()) || userId.equals(offer.getBuyerId()))
+                || offer.getLastModifiedById().equals(userId)) {
+            throw new UnauthorizedActionException("Not allowed to update this offer");
+        }
+
+        offer.setStatus(OtcOfferStatus.ACCEPTED);
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(userId);
+        otcOfferRepository.save(offer);
+    }
+
+    @Transactional
+    public void rejectOffer(Long offerId, Long userId) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!(userId.equals(offer.getSellerId()) || userId.equals(offer.getBuyerId()))
+                || offer.getLastModifiedById().equals(userId)) {
+            throw new UnauthorizedActionException("Not allowed to update this offer");
+        }
+
+        offer.setStatus(OtcOfferStatus.REJECTED);
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(userId);
+        otcOfferRepository.save(offer);
+    }
+
+    @Transactional
+    public void updateOffer(Long offerId, Long userId, CreateOtcOfferDto dto) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!(userId.equals(offer.getSellerId()) || userId.equals(offer.getBuyerId()))
+                || offer.getLastModifiedById().equals(userId)) {
+            throw new UnauthorizedActionException("Not allowed to update this offer");
+        }
+
+        offer.setAmount(dto.getAmount().intValue());
+        offer.setPricePerStock(dto.getPricePerStock());
+        offer.setPremium(dto.getPremium());
+        offer.setSettlementDate(dto.getSettlementDate());
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(userId);
+        offer.setStatus(OtcOfferStatus.PENDING);
+        otcOfferRepository.save(offer);
+    }
+
+    @Transactional
+    public void cancelOffer(Long offerId, Long userId) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        //  samo ako je korisnik poslednji modifikovao ponudu
+        if (!offer.getLastModifiedById().equals(userId)) {
+            throw new UnauthorizedActionException("Not allowed to cancel this offer");
+        }
+
+        offer.setStatus(OtcOfferStatus.CANCELLED);
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(userId);
+        otcOfferRepository.save(offer);
+    }
+
+
+    private String resolveUserName(Long userId) {
+        try {
+            ClientDto client = userClient.getClientById(userId);
+            return formatName(client.getFirstName(), client.getLastName());
+        } catch (Exception e1) {
+            try {
+                ActuaryDto actuary = userClient.getEmployeeById(userId);
+                return formatName(actuary.getFirstName(), actuary.getLastName());
+            } catch (Exception e2) {
+                return "Unknown User";
+            }
+        }
+    }
+
+    private String formatName(String firstName, String lastName) {
+        if (firstName == null && lastName == null) return "Unknown User";
+        return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : "");
+    }
+}
+
diff --git a/stock-service/src/main/java/rs/raf/stock_service/service/PortfolioService.java b/stock-service/src/main/java/rs/raf/stock_service/service/PortfolioService.java
index 88e8fe56..572a0840 100644
--- a/stock-service/src/main/java/rs/raf/stock_service/service/PortfolioService.java
+++ b/stock-service/src/main/java/rs/raf/stock_service/service/PortfolioService.java
@@ -6,10 +6,7 @@
 import org.springframework.data.crossstore.ChangeSetPersister;
 import org.springframework.stereotype.Service;
 import rs.raf.stock_service.client.UserClient;
-import rs.raf.stock_service.domain.dto.ClientDto;
-import rs.raf.stock_service.domain.dto.PortfolioEntryDto;
-import rs.raf.stock_service.domain.dto.PublicStockDto;
-import rs.raf.stock_service.domain.dto.SetPublicAmountDto;
+import rs.raf.stock_service.domain.dto.*;
 import rs.raf.stock_service.domain.entity.*;
 import rs.raf.stock_service.domain.enums.ListingType;
 import rs.raf.stock_service.domain.enums.OrderDirection;
@@ -18,7 +15,6 @@
 import rs.raf.stock_service.exceptions.PortfolioEntryNotFoundException;
 import rs.raf.stock_service.domain.entity.Order;
 import rs.raf.stock_service.domain.entity.PortfolioEntry;
-import rs.raf.stock_service.domain.dto.TaxGetResponseDto;
 import rs.raf.stock_service.domain.enums.TaxStatus;
 import rs.raf.stock_service.repository.*;
 import rs.raf.stock_service.domain.mapper.PortfolioMapper;
@@ -147,9 +143,10 @@ public List<PublicStockDto> getAllPublicStocks() {
             try {
                 ClientDto client = userClient.getClientById(entry.getUserId());
                 ownerName = client.getFirstName() + " " + client.getLastName();
+
             } catch (Exception e) {
-                log.warn("Could not fetch client info for userId: {}", entry.getUserId());
-                ownerName = "user-" + entry.getUserId(); // fallback
+                ActuaryDto actuary = userClient.getEmployeeById(entry.getUserId());
+                ownerName = actuary.getFirstName() + " " + actuary.getLastName();
             }
 
             BigDecimal currentPrice = listing.getPrice() != null ? listing.getPrice() : BigDecimal.ZERO;
@@ -165,9 +162,9 @@ public List<PublicStockDto> getAllPublicStocks() {
 
         }).collect(Collectors.toList());
     }
-    
+
     public TaxGetResponseDto getTaxes(Long userId){
-        
+
         List<Order> orders = orderRepository.findAllByUserId(userId);
         TaxGetResponseDto taxGetResponseDto = new TaxGetResponseDto();
 
diff --git a/stock-service/src/test/java/unit/OtcOfferServiceTest.java b/stock-service/src/test/java/unit/OtcOfferServiceTest.java
new file mode 100644
index 00000000..766054d2
--- /dev/null
+++ b/stock-service/src/test/java/unit/OtcOfferServiceTest.java
@@ -0,0 +1,276 @@
+package unit;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
+import rs.raf.stock_service.domain.dto.CreateOtcOfferDto;
+import rs.raf.stock_service.domain.dto.OtcOfferDto;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.domain.entity.PortfolioEntry;
+import rs.raf.stock_service.domain.entity.Stock;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+import rs.raf.stock_service.domain.mapper.OtcOfferMapper;
+import rs.raf.stock_service.exceptions.InvalidPublicAmountException;
+import rs.raf.stock_service.exceptions.PortfolioEntryNotFoundException;
+import rs.raf.stock_service.exceptions.UnauthorizedActionException;
+import rs.raf.stock_service.repository.OtcOfferRepository;
+import rs.raf.stock_service.repository.PortfolioEntryRepository;
+import rs.raf.stock_service.service.OtcService;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+
+@ExtendWith(MockitoExtension.class)
+public class OtcOfferServiceTest {
+
+    @Mock
+    private OtcOfferRepository otcOfferRepository;
+
+    @Mock
+    private PortfolioEntryRepository portfolioEntryRepository;
+
+    @Mock
+    private OtcOfferMapper otcOfferMapper;
+
+    @InjectMocks
+    private OtcService otcService;
+
+    private final Long buyerId = 10L;
+    private final Long sellerId = 20L;
+
+    private Stock stock;
+    private PortfolioEntry entry;
+    private CreateOtcOfferDto dto;
+
+    @BeforeEach
+    public void setup() {
+        stock = new Stock();
+        stock.setId(1L);
+
+        entry = new PortfolioEntry();
+        entry.setId(1L);
+        entry.setListing(stock);
+        entry.setUserId(sellerId);
+        entry.setPublicAmount(100);
+
+        dto = new CreateOtcOfferDto();
+        dto.setPortfolioEntryId(1L);
+        dto.setAmount(BigDecimal.valueOf(50));
+        dto.setPricePerStock(BigDecimal.valueOf(150));
+        dto.setPremium(BigDecimal.valueOf(10));
+        dto.setSettlementDate(LocalDate.now());
+    }
+
+    @Test
+    public void testCreateOffer_success() {
+        when(portfolioEntryRepository.findById(1L)).thenReturn(Optional.of(entry));
+        when(otcOfferRepository.save(any(OtcOffer.class))).thenAnswer(i -> i.getArguments()[0]);
+
+        when(otcOfferMapper.toDto(any(OtcOffer.class), eq(buyerId)))
+                .thenReturn(new OtcOfferDto());
+
+        OtcOfferDto result = otcService.createOffer(dto, buyerId);
+
+        assertNotNull(result);
+        verify(otcOfferRepository).save(any(OtcOffer.class));
+        verify(otcOfferMapper).toDto(any(OtcOffer.class), eq(buyerId));
+    }
+
+    @Test
+    public void testCreateOffer_portfolioEntryNotFound() {
+        when(portfolioEntryRepository.findById(1L)).thenReturn(Optional.empty());
+
+        assertThrows(PortfolioEntryNotFoundException.class,
+                () -> otcService.createOffer(dto, buyerId));
+    }
+
+    @Test
+    public void testCreateOffer_invalidPublicAmount() {
+        entry.setPublicAmount(10);
+        when(portfolioEntryRepository.findById(1L)).thenReturn(Optional.of(entry));
+
+        assertThrows(InvalidPublicAmountException.class,
+                () -> otcService.createOffer(dto, buyerId));
+    }
+
+    @Test
+    public void testAcceptOffer_authorized() {
+        OtcOffer offer = OtcOffer.builder()
+                .id(1L)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .lastModifiedById(sellerId)
+                .status(OtcOfferStatus.PENDING)
+                .build();
+
+        when(otcOfferRepository.findById(1L)).thenReturn(Optional.of(offer));
+
+        otcService.acceptOffer(1L, buyerId);
+
+        assertEquals(OtcOfferStatus.ACCEPTED, offer.getStatus());
+        assertEquals(buyerId, offer.getLastModifiedById());
+        assertNotNull(offer.getLastModified());
+        verify(otcOfferRepository).save(offer);
+    }
+
+    @Test
+    public void testAcceptOffer_unauthorized_sameLastModifier() {
+        OtcOffer offer = OtcOffer.builder()
+                .id(1L)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .lastModifiedById(buyerId)
+                .status(OtcOfferStatus.PENDING)
+                .build();
+
+        when(otcOfferRepository.findById(1L)).thenReturn(Optional.of(offer));
+
+        assertThrows(UnauthorizedActionException.class, () -> otcService.acceptOffer(1L, buyerId));
+        verify(otcOfferRepository, never()).save(any());
+    }
+
+    @Test
+    public void testRejectOffer_authorized() {
+        OtcOffer offer = OtcOffer.builder()
+                .id(1L)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .lastModifiedById(buyerId)
+                .status(OtcOfferStatus.PENDING)
+                .build();
+
+        when(otcOfferRepository.findById(1L)).thenReturn(Optional.of(offer));
+
+        otcService.rejectOffer(1L, sellerId);
+
+        assertEquals(OtcOfferStatus.REJECTED, offer.getStatus());
+        assertEquals(sellerId, offer.getLastModifiedById());
+        verify(otcOfferRepository).save(offer);
+    }
+
+    @Test
+    public void testUpdateOffer_authorized() {
+        Long offerId = 1L;
+
+        OtcOffer offer = OtcOffer.builder()
+                .id(offerId)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .lastModifiedById(sellerId)
+                .status(OtcOfferStatus.PENDING)
+                .build();
+
+        CreateOtcOfferDto dto = new CreateOtcOfferDto();
+        dto.setAmount(BigDecimal.TEN);
+        dto.setPricePerStock(new BigDecimal("100"));
+        dto.setPremium(new BigDecimal("5"));
+        dto.setSettlementDate(LocalDate.now().plusDays(7));
+
+        when(otcOfferRepository.findById(offerId)).thenReturn(Optional.of(offer));
+
+        otcService.updateOffer(offerId, buyerId, dto);
+
+        verify(otcOfferRepository).save(argThat(updated ->
+                updated.getAmount() == 10 &&
+                        updated.getPricePerStock().equals(dto.getPricePerStock()) &&
+                        updated.getPremium().equals(dto.getPremium()) &&
+                        updated.getSettlementDate().equals(dto.getSettlementDate()) &&
+                        updated.getLastModifiedById().equals(buyerId) &&
+                        updated.getStatus() == OtcOfferStatus.PENDING
+        ));
+    }
+
+    @Test
+    public void testUpdateOffer_unauthorized_sameModifier() {
+        Long offerId = 1L;
+
+        OtcOffer offer = OtcOffer.builder()
+                .id(offerId)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .lastModifiedById(buyerId)
+                .status(OtcOfferStatus.PENDING)
+                .build();
+
+        when(otcOfferRepository.findById(offerId)).thenReturn(Optional.of(offer));
+
+        assertThrows(UnauthorizedActionException.class, () -> otcService.updateOffer(offerId, buyerId, dto));
+        verify(otcOfferRepository, never()).save(any());
+    }
+
+    @Test
+    public void testGetAllActiveOffersForUser_returnsOffersSortedAndFiltered() {
+        Long userId = 100L;
+
+        OtcOffer offer1 = OtcOffer.builder()
+                .id(1L)
+                .buyerId(userId)
+                .sellerId(200L)
+                .lastModifiedById(200L)
+                .status(OtcOfferStatus.PENDING)
+                .lastModified(LocalDateTime.now().minusMinutes(10))
+                .build();
+
+        OtcOffer offer2 = OtcOffer.builder()
+                .id(2L)
+                .buyerId(300L)
+                .sellerId(userId)
+                .lastModifiedById(300L)
+                .status(OtcOfferStatus.PENDING)
+                .lastModified(LocalDateTime.now().minusMinutes(5))
+                .build();
+
+        OtcOffer offer3 = OtcOffer.builder()
+                .id(3L)
+                .buyerId(userId)
+                .sellerId(400L)
+                .lastModifiedById(userId)
+                .status(OtcOfferStatus.PENDING)
+                .lastModified(LocalDateTime.now().minusMinutes(1))
+                .build();
+
+        List<OtcOffer> offers = Arrays.asList(offer1, offer2, offer3);
+
+        when(otcOfferRepository.findAllByStatus(OtcOfferStatus.PENDING)).thenReturn(offers);
+
+
+        when(otcOfferMapper.toDto(any(OtcOffer.class), eq(userId)))
+                .thenAnswer(invocation -> {
+                    OtcOffer o = invocation.getArgument(0);
+                    return OtcOfferDto.builder()
+                            .id(o.getId())
+                            .amount(o.getAmount())
+                            .pricePerStock(o.getPricePerStock())
+                            .premium(o.getPremium())
+                            .settlementDate(o.getSettlementDate())
+                            .status(o.getStatus())
+                            .canInteract(!o.getLastModifiedById().equals(userId))
+                            .name("Mock User")
+                            .build();
+                });
+
+        List<OtcOfferDto> result = otcService.getAllActiveOffersForUser(userId);
+
+        assertEquals(3, result.size());
+        assertEquals(3L, result.get(0).getId()); // najskorije
+        assertEquals(2L, result.get(1).getId());
+        assertEquals(1L, result.get(2).getId());
+
+        assertFalse(result.get(0).getCanInteract()); // user je poslednji menjao
+        assertTrue(result.get(1).getCanInteract());
+        assertTrue(result.get(2).getCanInteract());
+    }
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**stock-service/src/main/java/rs/raf/stock_service/domain/dto/CreateOtcOfferDto.java:22**
* [nitpick] The field 'strikePrice' appears redundant since 'pricePerStock' is used in the service logic. Consider removing or renaming it for clarity and to avoid confusion.
```
private BigDecimal strikePrice;
```
**stock-service/src/main/java/rs/raf/stock_service/domain/dto/StockDto.java:7**
* [nitpick] Multiple Lombok annotations (@Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor, @Builder) are used in addition to @Data, which already provides getters, setters, and other functionality. Consider removing the redundant annotations to simplify the code.
```
@Getter
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T10:53:37Z
user: login: djzivkovic
body: testiraj na kraju da li mozes da:
1. kupacc salje ponudu
2. prodavac counter offeruje
3. kupac counter offeruje
4. prodavac counterofferuje
5. kupac prihvata
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T11:01:05Z
user: login: djzivkovic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T18:55:10Z
user: login: djzivkovic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T11:06:16Z
user: login: djzivkovic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:21:54Z
user: login: djzivkovic
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T21:30:26Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+package rs.raf.stock_service.domain.dto;
+
+import lombok.*;
+
+import javax.validation.constraints.DecimalMin;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Getter
+@Setter
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateOtcOfferDto {
+
+    private Long portfolioEntryId;
+
+    @DecimalMin("0.01")
+    private BigDecimal amount;
+
+    private BigDecimal strikePrice;
+
+    private LocalDate settlementDate;
+
+    private BigDecimal pricePerStock;
+
+    private BigDecimal premium;
path: stock-service/src/main/java/rs/raf/stock_service/domain/dto/CreateOtcOfferDto.java
user: login: djzivkovic
body: dodati validattion anotacije ovde i dodati @Valid u parametru controllera za ovaj dto

takodje strikeprice i priceperstock su ista stvar
created_at: 2025-04-06T10:55:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,132 @@
+package rs.raf.stock_service.service;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import rs.raf.stock_service.domain.dto.CreateOtcOfferDto;
+import rs.raf.stock_service.domain.dto.OtcOfferDto;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.domain.entity.PortfolioEntry;
+import rs.raf.stock_service.domain.entity.Stock;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+import rs.raf.stock_service.domain.mapper.OtcOfferMapper;
+import rs.raf.stock_service.exceptions.InvalidPublicAmountException;
+import rs.raf.stock_service.exceptions.PortfolioEntryNotFoundException;
+import rs.raf.stock_service.exceptions.UnauthorizedActionException;
+import rs.raf.stock_service.repository.OtcOfferRepository;
+import rs.raf.stock_service.repository.PortfolioEntryRepository;
+
+import javax.persistence.EntityNotFoundException;
+import javax.transaction.Transactional;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class OtcService {
+
+    private final OtcOfferRepository otcOfferRepository;
+    private final PortfolioEntryRepository portfolioEntryRepository;
+    private final OtcOfferMapper otcOfferMapper;
+
+
+    ///Klijent šalje novu ponudu
+
+    public OtcOfferDto createOffer(CreateOtcOfferDto dto, Long buyerId) {
+        PortfolioEntry sellerEntry = portfolioEntryRepository.findById(dto.getPortfolioEntryId())
+                .orElseThrow(PortfolioEntryNotFoundException::new);
+
+        Stock stock = (Stock) sellerEntry.getListing();
+        Long sellerId = sellerEntry.getUserId();
+
+        if (dto.getAmount().compareTo(BigDecimal.valueOf(sellerEntry.getPublicAmount())) > 0) {
+            throw new InvalidPublicAmountException("Not enough public shares to fulfill the offer");
+        }
+
+        OtcOffer offer = OtcOffer.builder()
+                .stock(stock)
+                .buyerId(buyerId)
+                .sellerId(sellerId)
+                .amount(dto.getAmount().intValue())
+                .pricePerStock(dto.getPricePerStock())
+                .premium(dto.getPremium())
+                .settlementDate(dto.getSettlementDate())
+                .status(OtcOfferStatus.PENDING)
+                .lastModified(LocalDateTime.now())
+                .lastModifiedById(buyerId)
+                .build();
+
+        return otcOfferMapper.toDto(otcOfferRepository.save(offer));
+    }
+
+
+
+    ///	Prodavac vidi sve aktivne ponude
+    public List<OtcOfferDto> getAllActiveOffersForSeller(Long sellerId) {
+        return otcOfferRepository.findAllBySellerIdAndStatus(sellerId, OtcOfferStatus.PENDING)
+                .stream()
+                .map(otcOfferMapper::toDto)
+                .collect(Collectors.toList());
+    }
+
+
+    /// Prodavac prihvata ponudu
+    @Transactional
+    public void acceptOffer(Long offerId, Long sellerId) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!offer.getSellerId().equals(sellerId)) {
+            throw new UnauthorizedActionException("Not the seller of this offer");
+        }
+
+        offer.setStatus(OtcOfferStatus.ACCEPTED);
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(sellerId);
+        otcOfferRepository.save(offer);
+    }
+
+
+
+    ///	Prodavac odbija ponudu
+    @Transactional
+    public void rejectOffer(Long offerId, Long sellerId) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!offer.getSellerId().equals(sellerId)) {
+            throw new UnauthorizedActionException("Not the seller of this offer");
+        }
+
+        offer.setStatus(OtcOfferStatus.REJECTED);
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(sellerId);
+        otcOfferRepository.save(offer);
+    }
+
+
+
+    /// Prodavac šalje kontra ponudu
+    @Transactional
+    public void updateOffer(Long offerId, Long sellerId, CreateOtcOfferDto dto) {
+        OtcOffer offer = otcOfferRepository.findById(offerId)
+                .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
+
+        if (!offer.getSellerId().equals(sellerId)) {
+            throw new UnauthorizedActionException("Not the seller of this offer");
+        }
+
+        offer.setAmount(dto.getAmount().intValue());
+        offer.setPricePerStock(dto.getPricePerStock());
+        offer.setPremium(dto.getPremium());
+        offer.setSettlementDate(dto.getSettlementDate());
+        offer.setLastModified(LocalDateTime.now());
+        offer.setLastModifiedById(sellerId);
+        offer.setStatus(OtcOfferStatus.PENDING);
+        otcOfferRepository.save(offer);
+    }
+}
path: stock-service/src/main/java/rs/raf/stock_service/service/OtcService.java
user: login: djzivkovic
body: svuda ovde ne treba proveravati da li je seller, jer i kupac moze da prihvati/odbije/counter offeruje (ako prvo seller counter offeruje), to tehnicki moze da ide zauvek, da se counter offeruju
created_at: 2025-04-06T10:59:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,56 +76,44 @@ public List<OtcOfferDto> getAllActiveOffersForSeller(Long sellerId) {
 
     /// Prodavac prihvata ponudu
     @Transactional
-    public void acceptOffer(Long offerId, Long sellerId) {
+    public void acceptOffer(Long offerId, Long userId) {
         OtcOffer offer = otcOfferRepository.findById(offerId)
                 .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
 
-        if (!offer.getSellerId().equals(sellerId)) {
-            throw new UnauthorizedActionException("Not the seller of this offer");
-        }
-
         offer.setStatus(OtcOfferStatus.ACCEPTED);
         offer.setLastModified(LocalDateTime.now());
-        offer.setLastModifiedById(sellerId);
+        offer.setLastModifiedById(userId);
         otcOfferRepository.save(offer);
     }
 
 
 
     ///	Prodavac odbija ponudu
     @Transactional
-    public void rejectOffer(Long offerId, Long sellerId) {
+    public void rejectOffer(Long offerId, Long userId) {
         OtcOffer offer = otcOfferRepository.findById(offerId)
                 .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
 
-        if (!offer.getSellerId().equals(sellerId)) {
-            throw new UnauthorizedActionException("Not the seller of this offer");
-        }
-
         offer.setStatus(OtcOfferStatus.REJECTED);
         offer.setLastModified(LocalDateTime.now());
-        offer.setLastModifiedById(sellerId);
+        offer.setLastModifiedById(userId);
         otcOfferRepository.save(offer);
     }
 
 
 
     /// Prodavac šalje kontra ponudu
     @Transactional
-    public void updateOffer(Long offerId, Long sellerId, CreateOtcOfferDto dto) {
+    public void updateOffer(Long offerId, Long userId, CreateOtcOfferDto dto) {
         OtcOffer offer = otcOfferRepository.findById(offerId)
                 .orElseThrow(() -> new EntityNotFoundException("Offer not found"));
 
-        if (!offer.getSellerId().equals(sellerId)) {
-            throw new UnauthorizedActionException("Not the seller of this offer");
-        }
-
path: stock-service/src/main/java/rs/raf/stock_service/service/OtcService.java
user: login: djzivkovic
body: mozes da vrattis svuda ovaj check ali proveravas jel (userId == seller || userId == buyer ) && lastmodifiedid != userId
created_at: 2025-04-06T18:55:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+package rs.raf.stock_service.controller;
+
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import rs.raf.stock_service.domain.dto.CreateOtcOfferDto;
+import rs.raf.stock_service.domain.dto.OtcOfferDto;
+import rs.raf.stock_service.domain.entity.OtcOffer;
+import rs.raf.stock_service.service.OtcService;
+import rs.raf.stock_service.utils.JwtTokenUtil;
+
+import javax.persistence.EntityNotFoundException;
+import javax.validation.Valid;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+@RestController
+@RequestMapping("/api/otc")
+@RequiredArgsConstructor
+public class OtcOfferController {
+
+    private final OtcService otcService;
+    private final JwtTokenUtil jwtTokenUtil;
+
+    @Operation(summary = "Create OTC offer", description = "Allows CLIENT or AGENT to create an OTC offer for a public stock.")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "201", description = "OTC offer created successfully."),
+            @ApiResponse(responseCode = "400", description = "Invalid input or insufficient shares."),
+            @ApiResponse(responseCode = "403", description = "Access denied."),
+            @ApiResponse(responseCode = "404", description = "Stock or seller not found."),
+            @ApiResponse(responseCode = "500", description = "Unexpected server error.")
+    })
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    @PostMapping
+    public ResponseEntity<?> createOtcOffer(@RequestHeader("Authorization") String authHeader,
+                                            @Valid @RequestBody CreateOtcOfferDto dto) {
+        try {
+            Long buyerId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            OtcOfferDto created = otcService.createOffer(dto, buyerId);
+            return ResponseEntity.status(HttpStatus.CREATED).body(created);
+        } catch (IllegalArgumentException e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        } catch (EntityNotFoundException e) {
+            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "Get all active OTC offers received by the user", description = "Returns pending offers where the user is the seller.")
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "Offers retrieved."),
+            @ApiResponse(responseCode = "204", description = "No active offers."),
+            @ApiResponse(responseCode = "403", description = "Access denied."),
+            @ApiResponse(responseCode = "500", description = "Unexpected server error.")
+    })
+    @PreAuthorize("hasAnyRole('CLIENT', 'AGENT')")
+    @GetMapping("/received")
+    public ResponseEntity<?> getReceivedOffers(@RequestHeader("Authorization") String authHeader) {
+        try {
+            Long userId = jwtTokenUtil.getUserIdFromAuthHeader(authHeader);
+            List<OtcOfferDto> offers = otcService.getAllActiveOffersForSeller(userId);
+            return ResponseEntity.ok(offers);
+        } catch (Exception e) {
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Server error: " + e.getMessage());
+        }
+    }
+
path: stock-service/src/main/java/rs/raf/stock_service/controller/OtcOfferController.java
user: login: djzivkovic
body: takodje sad sam se setio, ovo ne treba da vraca samo gde je user id seller nego i buyer, znaci treba da vidimo i primljene ponude, ali i poslate ponude, i sortiraj po last modified time da bude prvo najskorije modifikovan offer
created_at: 2025-04-07T11:06:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+package rs.raf.stock_service.domain.dto;
+
+import lombok.*;
+import rs.raf.stock_service.domain.enums.OtcOfferStatus;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OtcOfferDto {
+    private Long id;
+    private StockDto stock;
+    private Integer amount;
+    private BigDecimal pricePerStock;
+    private BigDecimal premium;
+    private LocalDate settlementDate;
+    private OtcOfferStatus status;
+
path: stock-service/src/main/java/rs/raf/stock_service/domain/dto/OtcOfferDto.java
user: login: djzivkovic
body: dodaj ovde:
- canInteract boolean koji je true ako lastModifiedById != userId koji zove endpoint
- name (ime sellera ako endpoint poziva buyer, a ime buyera ako endpoint poziva seller, tjs ime tog s kim user pregovara offer)
created_at: 2025-04-07T13:21:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: djzivkovic
created_at: 2025-04-07T16:20:32Z
body: dodati i cancel ako je last modiefied id == caller
author_association: CONTRIBUTOR
user: login: raf-si-2024-prow[bot]
created_at: 2025-04-08T21:30:31Z
body: LGTM label has been added.  <details>Git tree hash: 589cb8cc6ea6bfdb518bdebec71c6e078f53942c</details>
author_association: CONTRIBUTOR
user: login: djzivkovic
created_at: 2025-04-08T21:30:48Z
body: /approve
author_association: CONTRIBUTOR
user: login: raf-si-2024-prow[bot]
created_at: 2025-04-08T21:30:56Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/RAF-SI-2024/Banka-3-Backend/pull/213#issuecomment-2787709218" title="Approved">djzivkovic</a>*, *<a href="https://github.com/RAF-SI-2024/Banka-3-Backend/pull/213#" title="Author self-approved">Mihailo13222rn</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=RAF-SI-2024%2FBanka-3-Backend).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/RAF-SI-2024/Banka-3-Backend/blob/main/OWNERS)~~ [djzivkovic]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR