From 174afe68c50c97156e7ad6ec6c5f641f60e694fa Mon Sep 17 00:00:00 2001 From: gothsins Date: Mon, 31 Aug 2026 22:37:20 -0300 Subject: [PATCH 01/12] Add actuator dependency and expose health endpoint in SecurityConfig --- pom.xml | 5 +++++ .../java/com/gothsins/resolve/config/HazelcastConfig.java | 5 +++++ .../java/com/gothsins/resolve/security/SecurityConfig.java | 5 +++-- src/main/resources/application.properties | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 39060d1..22bc7da 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,11 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-actuator + + org.springframework.boot spring-boot-starter-security diff --git a/src/main/java/com/gothsins/resolve/config/HazelcastConfig.java b/src/main/java/com/gothsins/resolve/config/HazelcastConfig.java index cb225df..03f7b9d 100644 --- a/src/main/java/com/gothsins/resolve/config/HazelcastConfig.java +++ b/src/main/java/com/gothsins/resolve/config/HazelcastConfig.java @@ -10,6 +10,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.hazelcast.core.Hazelcast; @Configuration public class HazelcastConfig { @@ -27,6 +28,10 @@ public Config hazelcastConfiguration() { return config; } + @Bean + public HazelcastInstance hazelcastInstance(Config hazelcastConfiguration) { + return Hazelcast.newHazelcastInstance(hazelcastConfiguration); + } @Bean public IMap rateLimitMap( diff --git a/src/main/java/com/gothsins/resolve/security/SecurityConfig.java b/src/main/java/com/gothsins/resolve/security/SecurityConfig.java index 21e41b5..474a6a9 100644 --- a/src/main/java/com/gothsins/resolve/security/SecurityConfig.java +++ b/src/main/java/com/gothsins/resolve/security/SecurityConfig.java @@ -17,7 +17,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; @Configuration @@ -59,6 +59,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .exceptionHandling(ex -> ex.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/actuator/health").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); @@ -78,6 +79,6 @@ public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); - return (CorsConfigurationSource) source; + return source; } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87e56ed..fadc118 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,4 @@ spring.jpa.show-sql=false jwt.secret=${JWT_SECRET} jwt.expiration=86400000 logging.level.org.springframework.security=DEBUG +management.endpoints.web.exposure.include=health,metrics,info \ No newline at end of file From c0d3c934c61d7533a5d74acc8a9a2b0c81c8391d Mon Sep 17 00:00:00 2001 From: gothsins Date: Tue, 1 Sep 2026 23:51:27 -0300 Subject: [PATCH 02/12] Add Hazelcast health indicator and update management endpoint exposure --- .../config/HazelcastHealthIndicator.java | 25 +++++++++++++++++++ src/main/resources/application.properties | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gothsins/resolve/config/HazelcastHealthIndicator.java diff --git a/src/main/java/com/gothsins/resolve/config/HazelcastHealthIndicator.java b/src/main/java/com/gothsins/resolve/config/HazelcastHealthIndicator.java new file mode 100644 index 0000000..7060533 --- /dev/null +++ b/src/main/java/com/gothsins/resolve/config/HazelcastHealthIndicator.java @@ -0,0 +1,25 @@ +package com.gothsins.resolve.config; + +import com.hazelcast.core.HazelcastInstance; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.boot.health.contributor.Health; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HazelcastHealthIndicator implements HealthIndicator { + + private final HazelcastInstance hazelcastInstance; + + @Override + public Health health() { + if (hazelcastInstance.getLifecycleService().isRunning()) { + return Health.up() + .withDetail("cluster", hazelcastInstance.getConfig().getClusterName()) + .withDetail("members", hazelcastInstance.getCluster().getMembers().size()) + .build(); + } + return Health.down().build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index fadc118..89b744a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,4 @@ spring.jpa.show-sql=false jwt.secret=${JWT_SECRET} jwt.expiration=86400000 logging.level.org.springframework.security=DEBUG -management.endpoints.web.exposure.include=health,metrics,info \ No newline at end of file +management.endpoints.web.exposure.include=health,info \ No newline at end of file From 94d7533e273cbee256cc66a96ad837fb6d8e17e7 Mon Sep 17 00:00:00 2001 From: gothsins Date: Wed, 2 Sep 2026 17:43:41 -0300 Subject: [PATCH 03/12] Add MetricsService integration to TicketService and UserService for tracking ticket creation and user registration --- .../java/com/gothsins/resolve/service/TicketService.java | 8 ++++++++ .../java/com/gothsins/resolve/service/UserService.java | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gothsins/resolve/service/TicketService.java b/src/main/java/com/gothsins/resolve/service/TicketService.java index 69b7ad9..765ee3d 100644 --- a/src/main/java/com/gothsins/resolve/service/TicketService.java +++ b/src/main/java/com/gothsins/resolve/service/TicketService.java @@ -24,6 +24,7 @@ public class TicketService { private final UserRepository userRepository; private final CategoryRepository categoryRepository; private final TicketHistoryService ticketHistoryService; + private final MetricsService metricsService; @Transactional public TicketResponseDTO create(TicketRequestDTO dto) { @@ -52,6 +53,7 @@ public TicketResponseDTO create(TicketRequestDTO dto) { .build(); Ticket saved = ticketRepository.save(ticket); + metricsService.incrementTicketCreated(saved.getPriority()); return toResponseDTO(saved); } @@ -109,6 +111,7 @@ public TicketResponseDTO changeStatus(Long id, ChangeStatusDTO dto) { ticketHistoryService.registerChange( updated, user, "STATUS_CHANGE", oldStatus.name(), newStatus.name()); + metricsService.incrementTicketStatusChanged(oldStatus.name(), newStatus.name()); return toResponseDTO(updated); } @@ -149,6 +152,7 @@ private CategoryResponseDTO toCategoryDTO(Category category) { return CategoryResponseDTO.builder() .id(category.getId()) .name(category.getName()) + .active(category.getActive()) .build(); } @@ -156,6 +160,10 @@ private UserResponseDTO toUserDTO(User user) { return UserResponseDTO.builder() .id(user.getId()) .name(user.getName()) + .email(user.getEmail()) + .role(user.getRole()) + .active(user.getActive()) + .createdAt(user.getCreatedAt()) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/gothsins/resolve/service/UserService.java b/src/main/java/com/gothsins/resolve/service/UserService.java index c1cdf4a..63160d6 100644 --- a/src/main/java/com/gothsins/resolve/service/UserService.java +++ b/src/main/java/com/gothsins/resolve/service/UserService.java @@ -21,6 +21,7 @@ public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + private final MetricsService metricsService; @Transactional public UserResponseDTO create(UserRequestDTO dto) { @@ -31,7 +32,7 @@ public UserResponseDTO create(UserRequestDTO dto) { .build(); User saved = userRepository.save(user); - + metricsService.incrementUserRegistered(); return toResponseDTO(saved); } From c825aaf1c932ed9201f31d79df01257db5b0b3b6 Mon Sep 17 00:00:00 2001 From: gothsins Date: Wed, 2 Sep 2026 17:47:47 -0300 Subject: [PATCH 04/12] Add MetricsService for tracking ticket creation and user registration; set default active status in Category --- .../com/gothsins/resolve/entity/Category.java | 3 +- .../resolve/service/MetricsService.java | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gothsins/resolve/service/MetricsService.java diff --git a/src/main/java/com/gothsins/resolve/entity/Category.java b/src/main/java/com/gothsins/resolve/entity/Category.java index cd45434..4e22340 100644 --- a/src/main/java/com/gothsins/resolve/entity/Category.java +++ b/src/main/java/com/gothsins/resolve/entity/Category.java @@ -21,6 +21,7 @@ public class Category { @Column(nullable = false, unique = true) private String name; + @Builder.Default @Column(nullable = false) - private Boolean active; + private Boolean active = true; } \ No newline at end of file diff --git a/src/main/java/com/gothsins/resolve/service/MetricsService.java b/src/main/java/com/gothsins/resolve/service/MetricsService.java new file mode 100644 index 0000000..7544eb8 --- /dev/null +++ b/src/main/java/com/gothsins/resolve/service/MetricsService.java @@ -0,0 +1,28 @@ +package com.gothsins.resolve.service; + +import com.gothsins.resolve.entity.enums.TicketPriority; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Service; + +@Service +public class MetricsService { + + private final MeterRegistry meterRegistry; + + public MetricsService(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + public void incrementTicketCreated(TicketPriority priority) { + meterRegistry.counter("tickets.created", "priority", priority.name()).increment(); + } + + public void incrementTicketStatusChanged(String oldStatus, String newStatus) { + meterRegistry.counter("tickets.status.changed", + "from", oldStatus, "to", newStatus).increment(); + } + + public void incrementUserRegistered() { + meterRegistry.counter("users.registered").increment(); + } +} \ No newline at end of file From 3d1a7a58331d42358e692bbe8015de92e5957771 Mon Sep 17 00:00:00 2001 From: gothsins Date: Thu, 3 Sep 2026 19:57:14 -0300 Subject: [PATCH 05/12] Add Prometheus support by including micrometer-registry-prometheus dependency and updating management endpoint exposure --- pom.xml | 5 +++++ src/main/resources/application.properties | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 22bc7da..2f3b6c0 100644 --- a/pom.xml +++ b/pom.xml @@ -69,6 +69,11 @@ bucket4j_jdk17-hazelcast 8.19.0 + + io.micrometer + micrometer-registry-prometheus + runtime + com.bucket4j bucket4j_jdk17-core diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 89b744a..6aba255 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,4 +9,4 @@ spring.jpa.show-sql=false jwt.secret=${JWT_SECRET} jwt.expiration=86400000 logging.level.org.springframework.security=DEBUG -management.endpoints.web.exposure.include=health,info \ No newline at end of file +management.endpoints.web.exposure.include=health,info,prometheus \ No newline at end of file From bef65028826f92c35f2f77e01863f175b269bb4e Mon Sep 17 00:00:00 2001 From: gothsins Date: Fri, 4 Sep 2026 23:49:51 -0300 Subject: [PATCH 06/12] Add exception handler for malformed requests in GlobalExceptionHandler --- .../resolve/exception/GlobalExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/gothsins/resolve/exception/GlobalExceptionHandler.java b/src/main/java/com/gothsins/resolve/exception/GlobalExceptionHandler.java index 385e898..9b2a6e3 100644 --- a/src/main/java/com/gothsins/resolve/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/gothsins/resolve/exception/GlobalExceptionHandler.java @@ -3,6 +3,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -39,4 +40,11 @@ public ResponseEntity handleValidationErrors(MethodArgumentNotVal ErrorResponse error = new ErrorResponse(message, 400); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleMalformedRequest(HttpMessageNotReadableException ex) { + ErrorResponse error = new ErrorResponse( + "Corpo da requisição inválido ou ausente — confira o formato e os valores enviados (ex: prioridade deve ser LOW, MEDIUM, HIGH ou CRITICAL)", + HttpStatus.BAD_REQUEST.value()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); + } } From 457eca5e5d365b3a95f95663c3cf928c8ee2bb15 Mon Sep 17 00:00:00 2001 From: gothsins Date: Fri, 4 Sep 2026 23:50:36 -0300 Subject: [PATCH 07/12] Add SLA tracking with SlaService and integrate with MetricsService --- .../resolve/entity/enums/SlaStatus.java | 7 ++++ .../resolve/security/SecurityConfig.java | 2 + .../resolve/service/MetricsService.java | 33 +++++++++++++++- .../gothsins/resolve/service/SlaService.java | 39 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/gothsins/resolve/entity/enums/SlaStatus.java create mode 100644 src/main/java/com/gothsins/resolve/service/SlaService.java diff --git a/src/main/java/com/gothsins/resolve/entity/enums/SlaStatus.java b/src/main/java/com/gothsins/resolve/entity/enums/SlaStatus.java new file mode 100644 index 0000000..48472c4 --- /dev/null +++ b/src/main/java/com/gothsins/resolve/entity/enums/SlaStatus.java @@ -0,0 +1,7 @@ +package com.gothsins.resolve.entity.enums; + +public enum SlaStatus { + ON_TIME, + AT_RISK, + VIOLATED +} \ No newline at end of file diff --git a/src/main/java/com/gothsins/resolve/security/SecurityConfig.java b/src/main/java/com/gothsins/resolve/security/SecurityConfig.java index 474a6a9..bff36ad 100644 --- a/src/main/java/com/gothsins/resolve/security/SecurityConfig.java +++ b/src/main/java/com/gothsins/resolve/security/SecurityConfig.java @@ -60,6 +60,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/actuator/health").permitAll() + .requestMatchers("/actuator/prometheus").permitAll() + .requestMatchers("/api/auth/**", "/error").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/com/gothsins/resolve/service/MetricsService.java b/src/main/java/com/gothsins/resolve/service/MetricsService.java index 7544eb8..71ded15 100644 --- a/src/main/java/com/gothsins/resolve/service/MetricsService.java +++ b/src/main/java/com/gothsins/resolve/service/MetricsService.java @@ -1,6 +1,10 @@ package com.gothsins.resolve.service; +import com.gothsins.resolve.entity.Ticket; +import com.gothsins.resolve.entity.enums.SlaStatus; import com.gothsins.resolve.entity.enums.TicketPriority; +import com.gothsins.resolve.repository.TicketRepository; +import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service; @@ -8,9 +12,21 @@ public class MetricsService { private final MeterRegistry meterRegistry; + private final TicketRepository ticketRepository; + private final SlaService slaService; - public MetricsService(MeterRegistry meterRegistry) { + public MetricsService(MeterRegistry meterRegistry, TicketRepository ticketRepository, SlaService slaService) { this.meterRegistry = meterRegistry; + this.ticketRepository = ticketRepository; + this.slaService = slaService; + + Gauge.builder("tickets.sla.at_risk", this, MetricsService::countAtRiskTickets) + .description("Tickets abertos em risco de violar o SLA") + .register(meterRegistry); + + Gauge.builder("tickets.sla.violated", this, MetricsService::countViolatedTickets) + .description("Tickets abertos que já violaram o SLA") + .register(meterRegistry); } public void incrementTicketCreated(TicketPriority priority) { @@ -25,4 +41,19 @@ public void incrementTicketStatusChanged(String oldStatus, String newStatus) { public void incrementUserRegistered() { meterRegistry.counter("users.registered").increment(); } + + private double countAtRiskTickets() { + return countOpenTicketsByStatus(SlaStatus.AT_RISK); + } + + private double countViolatedTickets() { + return countOpenTicketsByStatus(SlaStatus.VIOLATED); + } + + private long countOpenTicketsByStatus(SlaStatus targetStatus) { + return ticketRepository.findAll().stream() + .filter(t -> t.getResolvedAt() == null && t.getClosedAt() == null) + .filter(t -> slaService.calculateStatus(t) == targetStatus) + .count(); + } } \ No newline at end of file diff --git a/src/main/java/com/gothsins/resolve/service/SlaService.java b/src/main/java/com/gothsins/resolve/service/SlaService.java new file mode 100644 index 0000000..9cc8c56 --- /dev/null +++ b/src/main/java/com/gothsins/resolve/service/SlaService.java @@ -0,0 +1,39 @@ +package com.gothsins.resolve.service; + +import com.gothsins.resolve.entity.Ticket; +import com.gothsins.resolve.entity.enums.SlaStatus; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDateTime; + +@Service +public class SlaService { + + private static final double AT_RISK_THRESHOLD = 0.2; + + public SlaStatus calculateStatus(Ticket ticket) { + LocalDateTime referenceTime = resolveReferenceTime(ticket); + + if (referenceTime.isAfter(ticket.getSlaDeadline())) { + return SlaStatus.VIOLATED; + } + + Duration total = ticket.getPriority().getSlaDuration(); + Duration remaining = Duration.between(referenceTime, ticket.getSlaDeadline()); + + double remainingRatio = (double) remaining.toMinutes() / total.toMinutes(); + + return remainingRatio <= AT_RISK_THRESHOLD ? SlaStatus.AT_RISK : SlaStatus.ON_TIME; + } + + private LocalDateTime resolveReferenceTime(Ticket ticket) { + if (ticket.getResolvedAt() != null) { + return ticket.getResolvedAt(); + } + if (ticket.getClosedAt() != null) { + return ticket.getClosedAt(); + } + return LocalDateTime.now(); + } +} \ No newline at end of file From 9f66ef476faa97b2e84c85a91f7779238973752e Mon Sep 17 00:00:00 2001 From: gothsins Date: Sun, 6 Sep 2026 23:17:46 -0300 Subject: [PATCH 08/12] Add SLA deadline to Ticket and enhance TicketPriority with SLA durations --- .../com/gothsins/resolve/entity/Ticket.java | 3 +++ .../resolve/entity/enums/TicketPriority.java | 20 +++++++++++++++---- .../resolve/service/MetricsService.java | 1 - 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/gothsins/resolve/entity/Ticket.java b/src/main/java/com/gothsins/resolve/entity/Ticket.java index ee34518..c02aae7 100644 --- a/src/main/java/com/gothsins/resolve/entity/Ticket.java +++ b/src/main/java/com/gothsins/resolve/entity/Ticket.java @@ -61,4 +61,7 @@ public class Ticket { @Column(name = "closed_at") private LocalDateTime closedAt; + + @Column(name = "sla_deadline", nullable = false) + private LocalDateTime slaDeadline; } \ No newline at end of file diff --git a/src/main/java/com/gothsins/resolve/entity/enums/TicketPriority.java b/src/main/java/com/gothsins/resolve/entity/enums/TicketPriority.java index 2e558ea..7fd4395 100644 --- a/src/main/java/com/gothsins/resolve/entity/enums/TicketPriority.java +++ b/src/main/java/com/gothsins/resolve/entity/enums/TicketPriority.java @@ -3,11 +3,23 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import java.time.Duration; + public enum TicketPriority { - LOW, - MEDIUM, - HIGH, - CRITICAL; + LOW(Duration.ofHours(72)), + MEDIUM(Duration.ofHours(48)), + HIGH(Duration.ofHours(24)), + CRITICAL(Duration.ofHours(12)); + + private final Duration slaDuration; + + TicketPriority(Duration slaDuration) { + this.slaDuration = slaDuration; + } + + public Duration getSlaDuration() { + return slaDuration; + } @JsonValue public String toValue() { diff --git a/src/main/java/com/gothsins/resolve/service/MetricsService.java b/src/main/java/com/gothsins/resolve/service/MetricsService.java index 71ded15..58412ae 100644 --- a/src/main/java/com/gothsins/resolve/service/MetricsService.java +++ b/src/main/java/com/gothsins/resolve/service/MetricsService.java @@ -1,6 +1,5 @@ package com.gothsins.resolve.service; -import com.gothsins.resolve.entity.Ticket; import com.gothsins.resolve.entity.enums.SlaStatus; import com.gothsins.resolve.entity.enums.TicketPriority; import com.gothsins.resolve.repository.TicketRepository; From c254eebb3d9764ee5e9c64d1ecab9411926d72b4 Mon Sep 17 00:00:00 2001 From: gothsins Date: Mon, 7 Sep 2026 21:56:41 -0300 Subject: [PATCH 09/12] Add SLA deadline and status to TicketResponseDTO; update TicketService to handle SLA logic --- src/main/java/com/gothsins/resolve/dto/TicketResponseDTO.java | 4 ++++ src/main/java/com/gothsins/resolve/service/TicketService.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/gothsins/resolve/dto/TicketResponseDTO.java b/src/main/java/com/gothsins/resolve/dto/TicketResponseDTO.java index 77b4fdf..a7d0654 100644 --- a/src/main/java/com/gothsins/resolve/dto/TicketResponseDTO.java +++ b/src/main/java/com/gothsins/resolve/dto/TicketResponseDTO.java @@ -1,4 +1,5 @@ package com.gothsins.resolve.dto; +import com.gothsins.resolve.entity.enums.SlaStatus; import com.gothsins.resolve.entity.enums.TicketPriority; import com.gothsins.resolve.entity.enums.TicketStatus; import lombok.*; @@ -25,4 +26,7 @@ public class TicketResponseDTO { private LocalDateTime updatedAt; private LocalDateTime resolvedAt; private LocalDateTime closedAt; + private LocalDateTime slaDeadline; + + private SlaStatus slaStatus; } diff --git a/src/main/java/com/gothsins/resolve/service/TicketService.java b/src/main/java/com/gothsins/resolve/service/TicketService.java index 765ee3d..9834e5c 100644 --- a/src/main/java/com/gothsins/resolve/service/TicketService.java +++ b/src/main/java/com/gothsins/resolve/service/TicketService.java @@ -25,6 +25,7 @@ public class TicketService { private final CategoryRepository categoryRepository; private final TicketHistoryService ticketHistoryService; private final MetricsService metricsService; + private final SlaService slaService; @Transactional public TicketResponseDTO create(TicketRequestDTO dto) { @@ -50,6 +51,7 @@ public TicketResponseDTO create(TicketRequestDTO dto) { .category(category) .requester(requester) .assignedAgent(assignedAgent) + .slaDeadline(LocalDateTime.now().plus(dto.getPriority().getSlaDuration())) .build(); Ticket saved = ticketRepository.save(ticket); @@ -145,6 +147,8 @@ private TicketResponseDTO toResponseDTO(Ticket ticket) { .updatedAt(ticket.getUpdatedAt()) .resolvedAt(ticket.getResolvedAt()) .closedAt(ticket.getClosedAt()) + .slaDeadline(ticket.getSlaDeadline()) + .slaStatus(slaService.calculateStatus(ticket)) .build(); } From 7f518f2d8df85f42a1ea746398ad207a6d7d9211 Mon Sep 17 00:00:00 2001 From: gothsins Date: Tue, 8 Sep 2026 19:07:22 -0300 Subject: [PATCH 10/12] Add resolution time tracking to MetricsService and update TicketService to record duration --- .../com/gothsins/resolve/service/MetricsService.java | 11 +++++++++++ .../com/gothsins/resolve/service/TicketService.java | 3 +++ 2 files changed, 14 insertions(+) diff --git a/src/main/java/com/gothsins/resolve/service/MetricsService.java b/src/main/java/com/gothsins/resolve/service/MetricsService.java index 58412ae..84a4372 100644 --- a/src/main/java/com/gothsins/resolve/service/MetricsService.java +++ b/src/main/java/com/gothsins/resolve/service/MetricsService.java @@ -5,8 +5,11 @@ import com.gothsins.resolve.repository.TicketRepository; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; import org.springframework.stereotype.Service; +import java.time.Duration; + @Service public class MetricsService { @@ -28,6 +31,14 @@ public MetricsService(MeterRegistry meterRegistry, TicketRepository ticketReposi .register(meterRegistry); } + public void recordResolutionTime(TicketPriority priority, Duration duration) { + Timer.builder("tickets.resolution.time") + .description("Tempo entre criação e resolução do ticket") + .tag("priority", priority.name()) + .register(meterRegistry) + .record(duration); + } + public void incrementTicketCreated(TicketPriority priority) { meterRegistry.counter("tickets.created", "priority", priority.name()).increment(); } diff --git a/src/main/java/com/gothsins/resolve/service/TicketService.java b/src/main/java/com/gothsins/resolve/service/TicketService.java index 9834e5c..b6be8f6 100644 --- a/src/main/java/com/gothsins/resolve/service/TicketService.java +++ b/src/main/java/com/gothsins/resolve/service/TicketService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -104,6 +105,8 @@ public TicketResponseDTO changeStatus(Long id, ChangeStatusDTO dto) { if (newStatus == TicketStatus.RESOLVED) { ticket.setResolvedAt(LocalDateTime.now()); + Duration resolutionTime = Duration.between(ticket.getCreatedAt(), ticket.getResolvedAt()); + metricsService.recordResolutionTime(ticket.getPriority(), resolutionTime); } if (newStatus == TicketStatus.CLOSED) { ticket.setClosedAt(LocalDateTime.now()); From a446610c96bd8d3cf3ee43aebc6e022592d732b6 Mon Sep 17 00:00:00 2001 From: gothsins Date: Tue, 8 Sep 2026 19:07:37 -0300 Subject: [PATCH 11/12] Add unit tests for UserService to validate user retrieval and exception handling --- .../com/gothsins/resolve/UserServiceTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/java/com/gothsins/resolve/UserServiceTest.java diff --git a/src/test/java/com/gothsins/resolve/UserServiceTest.java b/src/test/java/com/gothsins/resolve/UserServiceTest.java new file mode 100644 index 0000000..ffe6376 --- /dev/null +++ b/src/test/java/com/gothsins/resolve/UserServiceTest.java @@ -0,0 +1,65 @@ +package com.gothsins.resolve; + +import com.gothsins.resolve.dto.UserResponseDTO; +import com.gothsins.resolve.entity.User; +import com.gothsins.resolve.exception.ResourceNotFoundException; +import com.gothsins.resolve.repository.UserRepository; +import com.gothsins.resolve.service.UserService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + @Test + void testGetUserById() { + Long userId = 1L; + + User user = new User(); + user.setId(userId); + user.setName("Guiverme"); + user.setEmail("guiverme@email.com"); + + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + + UserResponseDTO result = userService.findById(userId); + + assertNotNull(result); + assertEquals(userId, result.getId()); + assertEquals("Guiverme", result.getName()); + assertEquals("guiverme@email.com", result.getEmail()); + + verify(userRepository).findById(userId); + } + + @Test + void shouldThrowExceptionWhenUserDoesNotExist() { + Long userId = 999L; + + when(userRepository.findById(userId)) + .thenReturn(Optional.empty()); + + assertThrows( + ResourceNotFoundException.class, + () -> userService.findById(userId) + ); + + verify(userRepository).findById(userId); + } +} \ No newline at end of file From 51cc729ab6ffd230d873d4a4b1e84d800de88615 Mon Sep 17 00:00:00 2001 From: gothsins Date: Fri, 11 Sep 2026 00:44:26 -0300 Subject: [PATCH 12/12] Add unit tests for UserService to validate user creation and retrieval --- .../com/gothsins/resolve/UserServiceTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/test/java/com/gothsins/resolve/UserServiceTest.java b/src/test/java/com/gothsins/resolve/UserServiceTest.java index ffe6376..8bbf9bf 100644 --- a/src/test/java/com/gothsins/resolve/UserServiceTest.java +++ b/src/test/java/com/gothsins/resolve/UserServiceTest.java @@ -1,14 +1,18 @@ package com.gothsins.resolve; +import com.gothsins.resolve.dto.UserRequestDTO; import com.gothsins.resolve.dto.UserResponseDTO; import com.gothsins.resolve.entity.User; import com.gothsins.resolve.exception.ResourceNotFoundException; import com.gothsins.resolve.repository.UserRepository; +import com.gothsins.resolve.service.MetricsService; import com.gothsins.resolve.service.UserService; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; @@ -23,6 +27,9 @@ class UserServiceTest { @Mock private UserRepository userRepository; + @Mock + private MetricsService metricsService; + @InjectMocks private UserService userService; @@ -62,4 +69,63 @@ void shouldThrowExceptionWhenUserDoesNotExist() { verify(userRepository).findById(userId); } + + @Test + void testCreateUserRequestDTO() { + + UserRequestDTO dto = new UserRequestDTO(); + + dto.setName("Guiverme"); + dto.setEmail("guiverme@email.com"); + dto.setPassword("p12345678"); + + User savedUser = new User(); + savedUser.setId(1L); + savedUser.setName(dto.getName()); + savedUser.setEmail(dto.getEmail()); + savedUser.setPassword(dto.getPassword()); + + Mockito.when(userRepository.save(Mockito.any(User.class))).thenReturn(savedUser); + + UserResponseDTO result = userService.create(dto); + + Assertions.assertNotNull(result); + Assertions.assertEquals(1L, result.getId()); + Assertions.assertEquals("Guiverme", result.getName()); + Assertions.assertEquals("guiverme@email.com", result.getEmail()); + + Mockito.verify(metricsService, Mockito.times(1)).incrementUserRegistered(); + } + + @Test + void testfindById() { + Long userId = 1L; + + User user = new User(); + user.setId(userId); + user.setName("Guiverme"); + user.setEmail("guiverme@email.com"); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + UserResponseDTO result = userService.findById(1L); + + Assertions.assertNotNull(result); + Assertions.assertEquals(1L, result.getId()); + Assertions.assertEquals("Guiverme", result.getName()); + Assertions.assertEquals("guiverme@email.com", result.getEmail()); + } + + @Test + void testFindById_UserNotFound_ThrowsException() { + Long nonExistingId = 999L; + + Mockito.when(userRepository.findById(nonExistingId)).thenReturn(Optional.empty()); + + RuntimeException exception = Assertions.assertThrows(RuntimeException.class, () -> { + userService.findById(nonExistingId); + }); + + Assertions.assertEquals("User not found", exception.getMessage()); + } } \ No newline at end of file