diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/TicketController.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/TicketController.java new file mode 100644 index 0000000..5820f9d --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/TicketController.java @@ -0,0 +1,75 @@ +package net.hackyourfuture.tickettrackingsystem.controller; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.dto.request.CreateTicketRequest; +import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateTicketRequest; +import net.hackyourfuture.tickettrackingsystem.dto.response.TicketResponse; +import net.hackyourfuture.tickettrackingsystem.service.TicketService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/tickets") +public class TicketController { + + private final TicketService ticketService; + + @PostMapping + public ResponseEntity createTicket( + @Valid @RequestBody CreateTicketRequest request + ) { + TicketResponse response = ticketService.createTicket(request); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping + public List getAllTickets() { + return ticketService.getAllTickets(); + } + + @GetMapping("/{id}") + public TicketResponse getTicketById(@PathVariable Long id) { + return ticketService.getTicketById(id); + } + + @PutMapping("/{id}") + public TicketResponse updateTicket( + @PathVariable Long id, + @Valid @RequestBody UpdateTicketRequest request + ) { + return ticketService.updateTicket(id, request); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteTicket(@PathVariable Long id) { + ticketService.deleteTicket(id); + + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{ticketId}/assignees/{userId}") + public ResponseEntity assignUserToTicket( + @PathVariable Long ticketId, + @PathVariable Long userId + ) { + ticketService.assignUserToTicket(ticketId, userId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{ticketId}/assignees/{userId}") + public ResponseEntity unassignUserFromTicket( + @PathVariable Long ticketId, + @PathVariable Long userId + ) { + ticketService.unassignUserFromTicket(ticketId, userId); + + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java index 013246e..f414dfa 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java @@ -5,7 +5,6 @@ import net.hackyourfuture.tickettrackingsystem.dto.request.CreateUserRequest; import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateUserRequest; import net.hackyourfuture.tickettrackingsystem.dto.response.UserResponse; -import net.hackyourfuture.tickettrackingsystem.model.User; import net.hackyourfuture.tickettrackingsystem.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -20,41 +19,35 @@ public class UserController { private final UserService userService; - private UserResponse toUserResponse(User user) { - return new UserResponse( - user.id(), - user.name(), - user.email() - ); - } @GetMapping public List getAllUsers() { - return userService.getAllUsers() - .stream() - .map(this::toUserResponse). - toList(); + return userService.getAllUsers(); } @GetMapping("/{id}") public UserResponse getUserById(@PathVariable Long id) { - return toUserResponse( - userService.getUserById(id) - ); + return userService.getUserById(id); } @PostMapping - public ResponseEntity createUser( @Valid @RequestBody CreateUserRequest request) { - User user = userService.createUser(request); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(toUserResponse(user)); + public ResponseEntity createUser( + @Valid @RequestBody CreateUserRequest request + ) { + UserResponse response = userService.createUser(request); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(response); } @PutMapping("/{id}") - public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { - User user = userService.updateUser(id, request); + public ResponseEntity updateUser( + @PathVariable Long id, + @Valid @RequestBody UpdateUserRequest request + ) { + UserResponse response = userService.updateUser(id, request); - return ResponseEntity.ok(toUserResponse(user)); + return ResponseEntity.ok(response); } @DeleteMapping("/{id}") @@ -63,4 +56,4 @@ public ResponseEntity deleteUser(@PathVariable Long id) { return ResponseEntity.noContent().build(); } -} +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateTicketRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateTicketRequest.java new file mode 100644 index 0000000..888db61 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateTicketRequest.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import net.hackyourfuture.tickettrackingsystem.model.TicketStatus; + +public record CreateTicketRequest( + @NotBlank String title, + String description, + @NotNull Long projectId, + @NotNull TicketStatus status +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateTicketRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateTicketRequest.java new file mode 100644 index 0000000..0291a7f --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateTicketRequest.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import net.hackyourfuture.tickettrackingsystem.model.TicketStatus; + +public record UpdateTicketRequest( + @NotBlank String title, + String description, + @NotNull Long projectId, + @NotNull TicketStatus status +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/ErrorResponse.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/ErrorResponse.java new file mode 100644 index 0000000..3e3edcf --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/ErrorResponse.java @@ -0,0 +1,14 @@ +package net.hackyourfuture.tickettrackingsystem.dto.response; + +import java.time.LocalDateTime; +import java.util.Map; + +public record ErrorResponse( + LocalDateTime timestamp, + int status, + String error, + String message, + String path, + Map validationErrors +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/TicketResponse.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/TicketResponse.java new file mode 100644 index 0000000..bec35e4 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/TicketResponse.java @@ -0,0 +1,18 @@ +package net.hackyourfuture.tickettrackingsystem.dto.response; + +import net.hackyourfuture.tickettrackingsystem.model.TicketStatus; + +import java.time.LocalDateTime; +import java.util.List; + +public record TicketResponse( + Long id, + String title, + String description, + Long projectId, + TicketStatus status, + List assignedUserIds, + LocalDateTime creationDate, + LocalDateTime updateDate +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java index 60d9e34..a74602d 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java @@ -1,28 +1,141 @@ package net.hackyourfuture.tickettrackingsystem.exception; +import jakarta.servlet.http.HttpServletRequest; +import net.hackyourfuture.tickettrackingsystem.dto.response.ErrorResponse; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.time.LocalDateTime; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity> handleResourceNotFound( - ResourceNotFoundException ex) { - - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(Map.of("message", ex.getMessage())); + public ResponseEntity handleResourceNotFoundException( + ResourceNotFoundException exception, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.NOT_FOUND, + exception.getMessage(), + request.getRequestURI(), + null + ); } @ExceptionHandler(DuplicateEmailException.class) - public ResponseEntity> handleDuplicateEmail( - DuplicateEmailException ex) { + public ResponseEntity handleDuplicateEmailException( + DuplicateEmailException exception, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.CONFLICT, + exception.getMessage(), + request.getRequestURI(), + null + ); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException exception, + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + exception.getMessage(), + request.getRequestURI(), + null + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException( + MethodArgumentNotValidException exception, + HttpServletRequest request + ) { + Map validationErrors = new LinkedHashMap<>(); + + for (FieldError fieldError : exception.getBindingResult().getFieldErrors()) { + validationErrors.putIfAbsent( + fieldError.getField(), + Objects.requireNonNullElse( + fieldError.getDefaultMessage(), + "Invalid value" + ) + ); + } + + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + "Validation failed", + request.getRequestURI(), + validationErrors + ); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException( + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.BAD_REQUEST, + "Invalid request body", + request.getRequestURI(), + null + ); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException( + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.CONFLICT, + "Database constraint violation", + request.getRequestURI(), + null + ); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException( + HttpServletRequest request + ) { + return buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "Something went wrong", + request.getRequestURI(), + null + ); + } + + private ResponseEntity buildErrorResponse( + HttpStatus status, + String message, + String path, + Map validationErrors + ) { + ErrorResponse response = new ErrorResponse( + LocalDateTime.now(), + status.value(), + status.getReasonPhrase(), + message, + path, + validationErrors + ); - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(Map.of("message", ex.getMessage())); + return ResponseEntity + .status(status) + .body(response); } } \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/model/Ticket.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/Ticket.java new file mode 100644 index 0000000..0001aaf --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/Ticket.java @@ -0,0 +1,14 @@ +package net.hackyourfuture.tickettrackingsystem.model; + +import java.time.LocalDateTime; + +public record Ticket( + Long id, + String title, + String description, + Long projectId, + TicketStatus status, + LocalDateTime creationDate, + LocalDateTime updateDate +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/model/TicketStatus.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/TicketStatus.java new file mode 100644 index 0000000..93f63a3 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/TicketStatus.java @@ -0,0 +1,21 @@ +package net.hackyourfuture.tickettrackingsystem.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TicketStatus { + + OPEN("open"), + IN_PROGRESS("in progress"), + CLOSED("closed"); + + private final String value; + + TicketStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/ProjectRepository.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/ProjectRepository.java index 800c8a7..cd95cd2 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/ProjectRepository.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/ProjectRepository.java @@ -37,4 +37,12 @@ public List findAllProjectSummaries(){ rs.getInt("in_progress_tickets"), rs.getInt("closed_tickets") ); + + public boolean existsById(Long id) { + String sql = "SELECT COUNT(*) FROM projects WHERE id = ?"; + + Integer count = jdbcTemplate.queryForObject(sql, Integer.class, id); + + return count != null && count > 0; + } } diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/TicketRepository.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/TicketRepository.java new file mode 100644 index 0000000..3ef93fa --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/TicketRepository.java @@ -0,0 +1,155 @@ +package net.hackyourfuture.tickettrackingsystem.repository; + +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.model.Ticket; +import net.hackyourfuture.tickettrackingsystem.model.TicketStatus; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class TicketRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper ticketRowMapper = (rs, rowNum) -> + new Ticket( + rs.getLong("id"), + rs.getString("title"), + rs.getString("description"), + rs.getLong("project_id"), + TicketStatus.valueOf(rs.getString("status")), + rs.getTimestamp("creation_date").toLocalDateTime(), + rs.getTimestamp("update_date") != null + ? rs.getTimestamp("update_date").toLocalDateTime() + : null + ); + + public Ticket create(Ticket ticket) { + String sql = """ + INSERT INTO tickets (title, description, project_id, status) + VALUES (?, ?, ?, ?::ticket_status) + RETURNING id, title, description, project_id, status, creation_date, update_date + """; + + return jdbcTemplate.queryForObject( + sql, + ticketRowMapper, + ticket.title(), + ticket.description(), + ticket.projectId(), + ticket.status().name() + ); + } + + public List findAll() { + String sql = """ + SELECT id, title, description, project_id, status, creation_date, update_date + FROM tickets + ORDER BY creation_date DESC + """; + + return jdbcTemplate.query(sql, ticketRowMapper); + } + + public Optional findById(Long id) { + String sql = """ + SELECT id, title, description, project_id, status, creation_date, update_date + FROM tickets + WHERE id = ? + """; + + try { + Ticket ticket = jdbcTemplate.queryForObject(sql, ticketRowMapper, id); + return Optional.of(ticket); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + public Ticket update(Ticket ticket) { + String sql = """ + UPDATE tickets + SET title = ?, + description = ?, + project_id = ?, + status = ?::ticket_status, + update_date = CURRENT_TIMESTAMP + WHERE id = ? + RETURNING id, title, description, project_id, status, creation_date, update_date + """; + + return jdbcTemplate.queryForObject( + sql, + ticketRowMapper, + ticket.title(), + ticket.description(), + ticket.projectId(), + ticket.status().name(), + ticket.id() + ); + } + + public boolean deleteById(Long id) { + String sql = "DELETE FROM tickets WHERE id = ?"; + + int rowsAffected = jdbcTemplate.update(sql, id); + + return rowsAffected > 0; + } + + public void assignUser(Long ticketId, Long userId) { + String sql = """ + INSERT INTO user_ticket (ticket_id, user_id) + VALUES (?, ?) + """; + + jdbcTemplate.update(sql, ticketId, userId); + } + + public boolean unassignUser(Long ticketId, Long userId) { + String sql = """ + DELETE FROM user_ticket + WHERE ticket_id = ? + AND user_id = ? + """; + + int rowsAffected = jdbcTemplate.update(sql, ticketId, userId); + + return rowsAffected > 0; + } + + public boolean assignmentExists(Long ticketId, Long userId) { + String sql = """ + SELECT COUNT(*) + FROM user_ticket + WHERE ticket_id = ? + AND user_id = ? + """; + + Integer count = jdbcTemplate.queryForObject( + sql, + Integer.class, + ticketId, + userId + ); + + return count != null && count > 0; + } + + public List findAssignedUserIds(Long ticketId) { + String sql = """ + SELECT user_id + FROM user_ticket + WHERE ticket_id = ? + ORDER BY user_id + """; + + return jdbcTemplate.queryForList(sql, Long.class, ticketId); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java index c2f03f8..f90d327 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java @@ -24,12 +24,21 @@ public class UserRepository { ); public List findAll() { - String sql = "SELECT id, name, email FROM users"; + String sql = """ + SELECT id, name, email + FROM users + ORDER BY id + """; + return jdbcTemplate.query(sql, userRowMapper); } public Optional findById(Long id) { - String sql = "SELECT id, name, email FROM users WHERE id = ?"; + String sql = """ + SELECT id, name, email + FROM users + WHERE id = ? + """; try { User user = jdbcTemplate.queryForObject(sql, userRowMapper, id); @@ -40,7 +49,11 @@ public Optional findById(Long id) { } public Optional findByEmail(String email) { - String sql = "SELECT id, name, email FROM users WHERE email = ?"; + String sql = """ + SELECT id, name, email + FROM users + WHERE email = ? + """; try { User user = jdbcTemplate.queryForObject(sql, userRowMapper, email); @@ -54,28 +67,28 @@ public User create(User user) { String sql = """ INSERT INTO users (name, email) VALUES (?, ?) - RETURNING id + RETURNING id, name, email """; - Long id = jdbcTemplate.queryForObject( + return jdbcTemplate.queryForObject( sql, - Long.class, + userRowMapper, user.name(), user.email() ); - - return new User(id, user.name(), user.email()); } - public void update(User user) { + public User update(User user) { String sql = """ - UPDATE users - SET name = ?, email = ? - WHERE id = ? - """; + UPDATE users + SET name = ?, email = ? + WHERE id = ? + RETURNING id, name, email + """; - jdbcTemplate.update( + return jdbcTemplate.queryForObject( sql, + userRowMapper, user.name(), user.email(), user.id() diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/service/TicketService.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/TicketService.java new file mode 100644 index 0000000..ce8ba8c --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/TicketService.java @@ -0,0 +1,146 @@ +package net.hackyourfuture.tickettrackingsystem.service; + +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.dto.request.CreateTicketRequest; +import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateTicketRequest; +import net.hackyourfuture.tickettrackingsystem.dto.response.TicketResponse; +import net.hackyourfuture.tickettrackingsystem.exception.ResourceNotFoundException; +import net.hackyourfuture.tickettrackingsystem.model.Ticket; +import net.hackyourfuture.tickettrackingsystem.repository.ProjectRepository; +import net.hackyourfuture.tickettrackingsystem.repository.TicketRepository; +import net.hackyourfuture.tickettrackingsystem.repository.UserRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class TicketService { + + private final TicketRepository ticketRepository; + private final ProjectRepository projectRepository; + private final UserRepository userRepository; + + public TicketResponse createTicket(CreateTicketRequest request) { + if (!projectRepository.existsById(request.projectId())) { + throw new ResourceNotFoundException( + "Project with id " + request.projectId() + " not found"); + } + + Ticket ticket = new Ticket( + null, + request.title(), + request.description(), + request.projectId(), + request.status(), + null, + null + ); + + Ticket createdTicket = ticketRepository.create(ticket); + + return toTicketResponse(createdTicket); + } + + public List getAllTickets() { + return ticketRepository.findAll() + .stream() + .map(this::toTicketResponse) + .toList(); + } + + public TicketResponse getTicketById(Long id) { + Ticket ticket = ticketRepository.findById(id) + .orElseThrow(() -> + new ResourceNotFoundException( + "Ticket with id " + id + " not found")); + + return toTicketResponse(ticket); + } + + public TicketResponse updateTicket(Long id, UpdateTicketRequest request) { + Ticket existingTicket = ticketRepository.findById(id) + .orElseThrow(() -> + new ResourceNotFoundException( + "Ticket with id " + id + " not found")); + + if (!projectRepository.existsById(request.projectId())) { + throw new ResourceNotFoundException( + "Project with id " + request.projectId() + " not found"); + } + + Ticket updatedTicket = new Ticket( + existingTicket.id(), + request.title(), + request.description(), + request.projectId(), + request.status(), + existingTicket.creationDate(), + existingTicket.updateDate() + ); + + Ticket savedTicket = ticketRepository.update(updatedTicket); + + return toTicketResponse(savedTicket); + } + + public void deleteTicket(Long id) { + boolean deleted = ticketRepository.deleteById(id); + + if (!deleted) { + throw new ResourceNotFoundException( + "Ticket with id " + id + " not found"); + } + } + + public void assignUserToTicket(Long ticketId, Long userId) { + if (ticketRepository.findById(ticketId).isEmpty()) { + throw new ResourceNotFoundException( + "Ticket with id " + ticketId + " not found"); + } + + if (userRepository.findById(userId).isEmpty()) { + throw new ResourceNotFoundException( + "User with id " + userId + " not found"); + } + + if (ticketRepository.assignmentExists(ticketId, userId)) { + throw new IllegalArgumentException( + "User " + userId + " is already assigned to ticket " + ticketId); + } + + ticketRepository.assignUser(ticketId, userId); + } + + public void unassignUserFromTicket(Long ticketId, Long userId) { + if (ticketRepository.findById(ticketId).isEmpty()) { + throw new ResourceNotFoundException( + "Ticket with id " + ticketId + " not found"); + } + + if (userRepository.findById(userId).isEmpty()) { + throw new ResourceNotFoundException( + "User with id " + userId + " not found"); + } + + boolean removed = ticketRepository.unassignUser(ticketId, userId); + + if (!removed) { + throw new ResourceNotFoundException( + "User " + userId + " is not assigned to ticket " + ticketId); + } + } + + private TicketResponse toTicketResponse(Ticket ticket) { + return new TicketResponse( + ticket.id(), + ticket.title(), + ticket.description(), + ticket.projectId(), + ticket.status(), + ticketRepository.findAssignedUserIds(ticket.id()), + ticket.creationDate(), + ticket.updateDate() + ); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java index e04cd4a..f8a79b7 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java @@ -1,14 +1,14 @@ package net.hackyourfuture.tickettrackingsystem.service; - import lombok.RequiredArgsConstructor; import net.hackyourfuture.tickettrackingsystem.dto.request.CreateUserRequest; import net.hackyourfuture.tickettrackingsystem.dto.request.UpdateUserRequest; +import net.hackyourfuture.tickettrackingsystem.dto.response.UserResponse; import net.hackyourfuture.tickettrackingsystem.exception.DuplicateEmailException; +import net.hackyourfuture.tickettrackingsystem.exception.ResourceNotFoundException; import net.hackyourfuture.tickettrackingsystem.model.User; import net.hackyourfuture.tickettrackingsystem.repository.UserRepository; import org.springframework.stereotype.Service; -import net.hackyourfuture.tickettrackingsystem.exception.ResourceNotFoundException; import java.util.List; import java.util.Optional; @@ -19,40 +19,38 @@ public class UserService { private final UserRepository userRepository; - public User createUser(CreateUserRequest request) { + public UserResponse createUser(CreateUserRequest request) { if (userRepository.findByEmail(request.email()).isPresent()) { throw new DuplicateEmailException( "Email " + request.email() + " already exists"); } - return userRepository.create(new User( + + User createdUser = userRepository.create(new User( null, request.name(), request.email() )); + + return toUserResponse(createdUser); } - public List getAllUsers() { - return userRepository.findAll(); + public List getAllUsers() { + return userRepository.findAll() + .stream() + .map(this::toUserResponse) + .toList(); } - public User getUserById(Long id) { - return userRepository.findById(id) + public UserResponse getUserById(Long id) { + User user = userRepository.findById(id) .orElseThrow(() -> - new ResourceNotFoundException( + new ResourceNotFoundException( "User with id " + id + " not found")); - } - public void deleteUser(Long id) { - boolean deleted = userRepository.deleteById(id); - - if (!deleted) { - throw new ResourceNotFoundException( - "User with id " + id + " not found"); - } + return toUserResponse(user); } - public User updateUser(Long id, UpdateUserRequest request) { - + public UserResponse updateUser(Long id, UpdateUserRequest request) { User existingUser = userRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException( @@ -73,8 +71,25 @@ public User updateUser(Long id, UpdateUserRequest request) { request.email() ); - userRepository.update(updatedUser); + User savedUser = userRepository.update(updatedUser); + + return toUserResponse(savedUser); + } - return updatedUser; + public void deleteUser(Long id) { + boolean deleted = userRepository.deleteById(id); + + if (!deleted) { + throw new ResourceNotFoundException( + "User with id " + id + " not found"); + } + } + + private UserResponse toUserResponse(User user) { + return new UserResponse( + user.id(), + user.name(), + user.email() + ); } -} +} \ No newline at end of file