From 5ec54416ec1b24ec2d7262a2a8eed2adabd72e32 Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 16 Jun 2026 15:07:19 +0200 Subject: [PATCH 1/4] Add initial database schema with tables and relationships --- src/main/resources/db/schema.sql | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/resources/db/schema.sql diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..e9950a2 --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,51 @@ +CREATE TYPE ticket_status AS ENUM ( + 'OPEN', 'IN PROGRESS', 'CLOSED'); + +CREATE TABLE "user" ( + id bigserial primary key , + name varchar(255) not null CHECK (char_length(name) >= 3), + email varchar(255) not null unique + +); +CREATE TABLE project ( + id bigserial primary key , + name varchar(255) not null,CHECK (char_length(name) >= 3) +); +CREATE TABLE ticket ( + id bigserial primary key , + title varchar(255) not null, + description varchar(255) , + project_id BIGINT NOT NULL, + status ticket_status not null, + creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_date TIMESTAMP, + CONSTRAINT fk_ticket_project + FOREIGN KEY (project_id) + REFERENCES project(id) + +); + +CREATE TABLE user_ticket ( + user_id BIGINT NOT NULL, + ticket_id BIGINT NOT NULL, + + primary key (user_id, ticket_id), + + CONSTRAINT fk_user_ticket_user + FOREIGN KEY (user_id) + REFERENCES "user"(id) + ON DELETE CASCADE, + CONSTRAINT fk_user_ticket_ticket + FOREIGN KEY (ticket_id) + REFERENCES ticket(id) + ON DELETE CASCADE + ); + +CREATE INDEX idx_tickets_status + ON ticket(status); + +CREATE INDEX idx_tickets_project_id + ON ticket(project_id); + +CREATE INDEX idx_user_ticket_user_id + ON user_ticket(user_id); \ No newline at end of file From bfeda97f4ce8f2a71ac8a187bd96dd0fc87ab77d Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 16 Jun 2026 20:44:09 +0200 Subject: [PATCH 2/4] Implement user management with CRUD operations and validation --- pom.xml | 20 +++- .../controller/UserController.java | 66 +++++++++++++ .../dto/request/CreateUserRequest.java | 13 +++ .../dto/request/UpdateUserRequest.java | 17 ++++ .../dto/response/UserResponse.java | 8 ++ .../exception/DuplicateEmailException.java | 8 ++ .../exception/GlobalExceptionHandler.java | 28 ++++++ .../exception/ResourceNotFoundException.java | 8 ++ .../tickettrackingsystem/model/User.java | 4 + .../repository/UserRepository.java | 98 +++++++++++++++++++ .../service/UserService.java | 80 +++++++++++++++ src/main/resources/application.properties | 6 ++ src/main/resources/db/schema.sql | 10 +- 13 files changed, 359 insertions(+), 7 deletions(-) create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java create mode 100644 src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java diff --git a/pom.xml b/pom.xml index 6890ffc..d262372 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,14 @@ org.springframework.boot spring-boot-starter - + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + org.postgresql postgresql @@ -45,7 +52,16 @@ spring-boot-starter-test test - + + org.projectlombok + lombok + provided + + + org.springframework.boot + spring-boot-starter-web + + diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java new file mode 100644 index 0000000..013246e --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/controller/UserController.java @@ -0,0 +1,66 @@ +package net.hackyourfuture.tickettrackingsystem.controller; + +import jakarta.validation.Valid; +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.model.User; +import net.hackyourfuture.tickettrackingsystem.service.UserService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +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(); + } + + @GetMapping("/{id}") + public UserResponse getUserById(@PathVariable Long id) { + return toUserResponse( + userService.getUserById(id) + ); + } + + @PostMapping + public ResponseEntity createUser( @Valid @RequestBody CreateUserRequest request) { + User user = userService.createUser(request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(toUserResponse(user)); + } + + @PutMapping("/{id}") + public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest request) { + User user = userService.updateUser(id, request); + + return ResponseEntity.ok(toUserResponse(user)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java new file mode 100644 index 0000000..50a64d0 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/CreateUserRequest.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record CreateUserRequest( + @NotBlank + @Size(min = 3) + String name, + @NotBlank + @Email + String email) { +} diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java new file mode 100644 index 0000000..f681080 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/request/UpdateUserRequest.java @@ -0,0 +1,17 @@ +package net.hackyourfuture.tickettrackingsystem.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record UpdateUserRequest( + + @NotBlank + @Size(min = 3) + String name, + + @NotBlank + @Email + String email + +) {} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java new file mode 100644 index 0000000..a6e8734 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/dto/response/UserResponse.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.dto.response; + +public record UserResponse( + Long id, + String name, + String email +) { +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java new file mode 100644 index 0000000..a7b3025 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/DuplicateEmailException.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +public class DuplicateEmailException extends RuntimeException { + + public DuplicateEmailException(String message) { + super(message); + } +} \ 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 new file mode 100644 index 0000000..60d9e34 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/GlobalExceptionHandler.java @@ -0,0 +1,28 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFound( + ResourceNotFoundException ex) { + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", ex.getMessage())); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity> handleDuplicateEmail( + DuplicateEmailException ex) { + + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(Map.of("message", ex.getMessage())); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..dc07743 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package net.hackyourfuture.tickettrackingsystem.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java new file mode 100644 index 0000000..d22fd58 --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/model/User.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.tickettrackingsystem.model; + +public record User(Long id, String name, String email) { +} \ 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 new file mode 100644 index 0000000..37fda1e --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java @@ -0,0 +1,98 @@ +package net.hackyourfuture.tickettrackingsystem.repository; + +import lombok.RequiredArgsConstructor; +import net.hackyourfuture.tickettrackingsystem.model.User; +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 UserRepository { + + private final JdbcTemplate jdbcTemplate; + + private final RowMapper userRowMapper = (rs, rowNum) -> + new User( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email") + ); + + public List findAll() { + String sql = "SELECT id, name, email FROM users"; + return jdbcTemplate.query(sql, userRowMapper); + } + + public Optional findById(Long id) { + String sql = "SELECT id, name, email FROM users WHERE id = ?"; + + try { + User user = jdbcTemplate.queryForObject(sql, userRowMapper, id); + return Optional.of(user); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + public Optional findByEmail(String email) { + String sql = "SELECT id, name, email FROM users WHERE email = ?"; + + try { + User user = jdbcTemplate.queryForObject(sql, userRowMapper, email); + return Optional.of(user); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + public User create(User user) { + String sql = """ + INSERT INTO users (name, email) + VALUES (?, ?) + RETURNING id + """; + + Long id = jdbcTemplate.queryForObject( + sql, + Long.class, + user.name(), + user.email() + ); + + return new User(id, user.name(), user.email()); + } + + public Optional update(User user) { + String sql = """ + UPDATE users + SET name = ?, email = ? + WHERE id = ? + """; + + int rowsAffected = jdbcTemplate.update( + sql, + user.name(), + user.email(), + user.id() + ); + + if (rowsAffected == 0) { + return Optional.empty(); + } + + return Optional.of(user); + } + + public boolean deleteById(Long id) { + String sql = "DELETE FROM users WHERE id = ?"; + + int rowsAffected = jdbcTemplate.update(sql, id); + + return rowsAffected > 0; + } +} \ 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 new file mode 100644 index 0000000..e04cd4a --- /dev/null +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/service/UserService.java @@ -0,0 +1,80 @@ +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.exception.DuplicateEmailException; +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; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + public User createUser(CreateUserRequest request) { + if (userRepository.findByEmail(request.email()).isPresent()) { + throw new DuplicateEmailException( + "Email " + request.email() + " already exists"); + } + return userRepository.create(new User( + null, + request.name(), + request.email() + )); + } + + public List getAllUsers() { + return userRepository.findAll(); + } + + public User getUserById(Long id) { + return userRepository.findById(id) + .orElseThrow(() -> + 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"); + } + } + + public User updateUser(Long id, UpdateUserRequest request) { + + User existingUser = userRepository.findById(id) + .orElseThrow(() -> + new ResourceNotFoundException( + "User with id " + id + " not found")); + + Optional userWithSameEmail = + userRepository.findByEmail(request.email()); + + if (userWithSameEmail.isPresent() + && !userWithSameEmail.get().id().equals(id)) { + throw new DuplicateEmailException( + "Email " + request.email() + " already exists"); + } + + User updatedUser = new User( + existingUser.id(), + request.name(), + request.email() + ); + + userRepository.update(updatedUser); + + return updatedUser; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 26d4cb5..3145487 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ spring.application.name=ticket-tracking-system + +spring.datasource.url=jdbc:postgresql://localhost:5432/ticket_tracker +spring.datasource.username=ticket_user +spring.datasource.password=ticket_password +spring.datasource.driver-class-name=org.postgresql.Driver + diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql index e9950a2..317d08b 100644 --- a/src/main/resources/db/schema.sql +++ b/src/main/resources/db/schema.sql @@ -1,17 +1,17 @@ CREATE TYPE ticket_status AS ENUM ( - 'OPEN', 'IN PROGRESS', 'CLOSED'); + 'OPEN', 'IN_PROGRESS', 'CLOSED'); -CREATE TABLE "user" ( +CREATE TABLE users ( id bigserial primary key , name varchar(255) not null CHECK (char_length(name) >= 3), email varchar(255) not null unique ); -CREATE TABLE project ( +CREATE TABLE projects ( id bigserial primary key , name varchar(255) not null,CHECK (char_length(name) >= 3) ); -CREATE TABLE ticket ( +CREATE TABLE tickets ( id bigserial primary key , title varchar(255) not null, description varchar(255) , @@ -33,7 +33,7 @@ CREATE TABLE user_ticket ( CONSTRAINT fk_user_ticket_user FOREIGN KEY (user_id) - REFERENCES "user"(id) + REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_user_ticket_ticket FOREIGN KEY (ticket_id) From 7704c07a47b6a59c523a16c882257f0208973a0e Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 16 Jun 2026 20:51:02 +0200 Subject: [PATCH 3/4] Refactor `update` method in `UserRepository` to return `void` instead of `Optional` --- .../repository/UserRepository.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java index 37fda1e..c2f03f8 100644 --- a/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java +++ b/src/main/java/net/hackyourfuture/tickettrackingsystem/repository/UserRepository.java @@ -67,25 +67,19 @@ INSERT INTO users (name, email) return new User(id, user.name(), user.email()); } - public Optional update(User user) { + public void update(User user) { String sql = """ - UPDATE users - SET name = ?, email = ? - WHERE id = ? - """; + UPDATE users + SET name = ?, email = ? + WHERE id = ? + """; - int rowsAffected = jdbcTemplate.update( + jdbcTemplate.update( sql, user.name(), user.email(), user.id() ); - - if (rowsAffected == 0) { - return Optional.empty(); - } - - return Optional.of(user); } public boolean deleteById(Long id) { From 9a83ed5d3a254c91e4986abbc9c327725b26937e Mon Sep 17 00:00:00 2001 From: Shadi Date: Tue, 16 Jun 2026 21:09:13 +0200 Subject: [PATCH 4/4] Configure Lombok annotation processing for Maven --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index d262372..ff263ee 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,21 @@ org.springframework.boot spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + + org.projectlombok + lombok + 1.18.42 + + + +