From 640bcf31f49e9d840b43f83ffb2a7fc77ceecaf3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:17:33 +0000 Subject: [PATCH 1/5] feat: add manual interview count update via API and MCP Adds PATCH /api/v1/dashboard/interview-count endpoint and Update-Interview-Count MCP tool so users can correct the cumulative interview counter when the automatic detection misses transitions. https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../controller/DashboardController.java | 29 ++++++++++++++++++- .../UpdateInterviewCountRequest.java | 7 +++++ .../mcp/tools/McpAnalyticsTools.java | 22 ++++++++++++++ .../service/InterviewMetricsService.java | 21 +++++++++++++- 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java diff --git a/src/main/java/com/jobtracker/controller/DashboardController.java b/src/main/java/com/jobtracker/controller/DashboardController.java index 1556d961..48edda87 100644 --- a/src/main/java/com/jobtracker/controller/DashboardController.java +++ b/src/main/java/com/jobtracker/controller/DashboardController.java @@ -1,14 +1,20 @@ package com.jobtracker.controller; import com.jobtracker.dto.dashboard.DashboardSummaryResponse; +import com.jobtracker.dto.dashboard.UpdateInterviewCountRequest; import com.jobtracker.service.DashboardService; +import com.jobtracker.service.InterviewMetricsService; +import com.jobtracker.util.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,9 +24,15 @@ public class DashboardController { private final DashboardService dashboardService; + private final InterviewMetricsService interviewMetricsService; + private final SecurityUtils securityUtils; - public DashboardController(DashboardService dashboardService) { + public DashboardController(DashboardService dashboardService, + InterviewMetricsService interviewMetricsService, + SecurityUtils securityUtils) { this.dashboardService = dashboardService; + this.interviewMetricsService = interviewMetricsService; + this.securityUtils = securityUtils; } @Operation( @@ -36,4 +48,19 @@ public DashboardController(DashboardService dashboardService) { public ResponseEntity getSummary() { return ResponseEntity.ok(dashboardService.getSummary()); } + + @Operation( + summary = "Update interview count", + description = "Manually sets the cumulative interview count for the authenticated user", + responses = { + @ApiResponse(responseCode = "204", description = "Count updated"), + @ApiResponse(responseCode = "400", description = "Invalid count value"), + @ApiResponse(responseCode = "401", description = "Not authenticated") + } + ) + @PatchMapping("/interview-count") + public ResponseEntity updateInterviewCount(@Valid @RequestBody UpdateInterviewCountRequest request) { + interviewMetricsService.setInterviewCount(securityUtils.getCurrentUserId(), request.count()); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java new file mode 100644 index 00000000..49571bdb --- /dev/null +++ b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java @@ -0,0 +1,7 @@ +package com.jobtracker.dto.dashboard; + +import jakarta.validation.constraints.Min; + +public record UpdateInterviewCountRequest( + @Min(0) long count +) {} diff --git a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java index 60311c6d..ab2a4d08 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java @@ -9,6 +9,7 @@ import com.jobtracker.mapper.ApplicationMapper; import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.service.ApplicationService; +import com.jobtracker.service.InterviewMetricsService; import com.jobtracker.util.SecurityUtils; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpTool.McpAnnotations; @@ -33,18 +34,39 @@ public class McpAnalyticsTools { private final ApplicationRepository applicationRepository; private final ApplicationMapper applicationMapper; private final ApplicationService applicationService; + private final InterviewMetricsService interviewMetricsService; private final SecurityUtils securityUtils; public McpAnalyticsTools(ApplicationRepository applicationRepository, ApplicationMapper applicationMapper, ApplicationService applicationService, + InterviewMetricsService interviewMetricsService, SecurityUtils securityUtils) { this.applicationRepository = applicationRepository; this.applicationMapper = applicationMapper; this.applicationService = applicationService; + this.interviewMetricsService = interviewMetricsService; this.securityUtils = securityUtils; } + @McpTool( + name = "Update-Interview-Count", + title = "Update Interview Count", + description = "Manually sets the cumulative interview count for the current user. Use this when the automatic counter missed interviews or needs correction.", + annotations = @McpAnnotations( + title = "Update Interview Count", + readOnlyHint = false, + destructiveHint = false, + idempotentHint = true, + openWorldHint = false)) + @Transactional + public String updateInterviewCount( + @McpToolParam(required = true, description = "The new total interview count (must be >= 0)") long count) { + UUID userId = securityUtils.getCurrentUserId(); + interviewMetricsService.setInterviewCount(userId, count); + return "Interview count updated to " + count; + } + @McpTool( name = "Get-Analytics", title = "Get Analytics", diff --git a/src/main/java/com/jobtracker/service/InterviewMetricsService.java b/src/main/java/com/jobtracker/service/InterviewMetricsService.java index f1f2e64e..188d8e8d 100644 --- a/src/main/java/com/jobtracker/service/InterviewMetricsService.java +++ b/src/main/java/com/jobtracker/service/InterviewMetricsService.java @@ -7,6 +7,7 @@ import com.jobtracker.entity.enums.ApplicationStatus; import com.jobtracker.repository.InterviewEventRepository; import com.jobtracker.repository.UserInterviewMetricsRepository; +import com.jobtracker.repository.UserRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,11 +30,14 @@ public class InterviewMetricsService { private final UserInterviewMetricsRepository metricsRepository; private final InterviewEventRepository eventRepository; + private final UserRepository userRepository; public InterviewMetricsService(UserInterviewMetricsRepository metricsRepository, - InterviewEventRepository eventRepository) { + InterviewEventRepository eventRepository, + UserRepository userRepository) { this.metricsRepository = metricsRepository; this.eventRepository = eventRepository; + this.userRepository = userRepository; } public boolean isInterviewStatus(String status) { @@ -77,6 +81,21 @@ public void recordStatusTransition(JobApplication application, eventRepository.save(event); } + @Transactional + public void setInterviewCount(UUID userId, long count) { + if (count < 0) throw new IllegalArgumentException("Interview count cannot be negative"); + UserInterviewMetrics metrics = metricsRepository.findByUser_Id(userId) + .orElseGet(() -> { + User user = userRepository.getReferenceById(userId); + UserInterviewMetrics created = new UserInterviewMetrics(); + created.setUser(user); + created.setInterviewCount(0); + return created; + }); + metrics.setInterviewCount(count); + metricsRepository.save(metrics); + } + @Transactional(readOnly = true) public long getInterviewCount(UUID userId) { return metricsRepository.findById(userId) From 1d0691bce704f7ab4da602e4db5f538ea9e805de Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 15:34:04 +0000 Subject: [PATCH 2/5] feat: move interview count to per-application field Adds interview_count column to job_applications so each application tracks its own count. Dashboard interviewCount now sums across all applications. Removes the per-user UserInterviewMetrics path and the manual update endpoint/MCP tool (editing is now done via the application form). https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../controller/DashboardController.java | 29 +---------- .../dto/application/ApplicationRequest.java | 7 ++- .../dto/application/ApplicationResponse.java | 2 + .../UpdateInterviewCountRequest.java | 7 --- .../com/jobtracker/entity/JobApplication.java | 6 +++ .../jobtracker/mapper/ApplicationMapper.java | 1 + .../mcp/tools/McpAnalyticsTools.java | 22 -------- .../repository/ApplicationRepository.java | 3 ++ .../service/ApplicationService.java | 3 ++ .../service/InterviewMetricsService.java | 52 +++---------------- ...dd_interview_count_to_job_applications.sql | 2 + 11 files changed, 31 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java create mode 100644 src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql diff --git a/src/main/java/com/jobtracker/controller/DashboardController.java b/src/main/java/com/jobtracker/controller/DashboardController.java index 48edda87..1556d961 100644 --- a/src/main/java/com/jobtracker/controller/DashboardController.java +++ b/src/main/java/com/jobtracker/controller/DashboardController.java @@ -1,20 +1,14 @@ package com.jobtracker.controller; import com.jobtracker.dto.dashboard.DashboardSummaryResponse; -import com.jobtracker.dto.dashboard.UpdateInterviewCountRequest; import com.jobtracker.service.DashboardService; -import com.jobtracker.service.InterviewMetricsService; -import com.jobtracker.util.SecurityUtils; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,15 +18,9 @@ public class DashboardController { private final DashboardService dashboardService; - private final InterviewMetricsService interviewMetricsService; - private final SecurityUtils securityUtils; - public DashboardController(DashboardService dashboardService, - InterviewMetricsService interviewMetricsService, - SecurityUtils securityUtils) { + public DashboardController(DashboardService dashboardService) { this.dashboardService = dashboardService; - this.interviewMetricsService = interviewMetricsService; - this.securityUtils = securityUtils; } @Operation( @@ -48,19 +36,4 @@ public DashboardController(DashboardService dashboardService, public ResponseEntity getSummary() { return ResponseEntity.ok(dashboardService.getSummary()); } - - @Operation( - summary = "Update interview count", - description = "Manually sets the cumulative interview count for the authenticated user", - responses = { - @ApiResponse(responseCode = "204", description = "Count updated"), - @ApiResponse(responseCode = "400", description = "Invalid count value"), - @ApiResponse(responseCode = "401", description = "Not authenticated") - } - ) - @PatchMapping("/interview-count") - public ResponseEntity updateInterviewCount(@Valid @RequestBody UpdateInterviewCountRequest request) { - interviewMetricsService.setInterviewCount(securityUtils.getCurrentUserId(), request.count()); - return ResponseEntity.noContent().build(); - } } diff --git a/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java b/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java index e66283bc..27f1a5ed 100644 --- a/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java +++ b/src/main/java/com/jobtracker/dto/application/ApplicationRequest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.PastOrPresent; import jakarta.validation.constraints.Pattern; @@ -53,5 +54,9 @@ public record ApplicationRequest( String note, @Schema(description = "Platform or job board where the vacancy was found", example = "LinkedIn") - String platform + String platform, + + @Schema(description = "Number of interviews held for this application", example = "2") + @Min(0) + Integer interviewCount ) {} diff --git a/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java b/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java index 27945649..d70b340f 100644 --- a/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java +++ b/src/main/java/com/jobtracker/dto/application/ApplicationResponse.java @@ -60,6 +60,8 @@ public record ApplicationResponse( @Schema(description = "Timestamp when the latest Google Docs resume was generated", example = "2024-06-11T14:45:00") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime driveResumeGeneratedAt, + @Schema(description = "Number of interviews held for this application", example = "2") + int interviewCount, @Schema(description = "Record creation timestamp", example = "2024-06-01T10:00:00") @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") LocalDateTime createdAt, diff --git a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java b/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java deleted file mode 100644 index 49571bdb..00000000 --- a/src/main/java/com/jobtracker/dto/dashboard/UpdateInterviewCountRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.jobtracker.dto.dashboard; - -import jakarta.validation.constraints.Min; - -public record UpdateInterviewCountRequest( - @Min(0) long count -) {} diff --git a/src/main/java/com/jobtracker/entity/JobApplication.java b/src/main/java/com/jobtracker/entity/JobApplication.java index 7613c5de..698fd905 100644 --- a/src/main/java/com/jobtracker/entity/JobApplication.java +++ b/src/main/java/com/jobtracker/entity/JobApplication.java @@ -43,6 +43,9 @@ public class JobApplication { @Column(name = "interview_scheduled", nullable = false) private boolean interviewScheduled; + @Column(name = "interview_count", nullable = false) + private int interviewCount = 0; + @Column(name = "next_step_date_time") private LocalDateTime nextStepDateTime; @@ -147,6 +150,9 @@ protected void onUpdate() { public boolean isInterviewScheduled() { return interviewScheduled; } public void setInterviewScheduled(boolean interviewScheduled) { this.interviewScheduled = interviewScheduled; } + public int getInterviewCount() { return interviewCount; } + public void setInterviewCount(int interviewCount) { this.interviewCount = interviewCount; } + public LocalDateTime getNextStepDateTime() { return nextStepDateTime; } public void setNextStepDateTime(LocalDateTime nextStepDateTime) { this.nextStepDateTime = nextStepDateTime; } diff --git a/src/main/java/com/jobtracker/mapper/ApplicationMapper.java b/src/main/java/com/jobtracker/mapper/ApplicationMapper.java index a330b0cc..48fedfb4 100644 --- a/src/main/java/com/jobtracker/mapper/ApplicationMapper.java +++ b/src/main/java/com/jobtracker/mapper/ApplicationMapper.java @@ -31,6 +31,7 @@ public ApplicationResponse toResponse(JobApplication app) { app.getDriveResumeFileName(), app.getDriveResumeDocumentUrl(), app.getDriveResumeGeneratedAt(), + app.getInterviewCount(), app.getCreatedAt(), app.getUpdatedAt() ); diff --git a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java index ab2a4d08..60311c6d 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpAnalyticsTools.java @@ -9,7 +9,6 @@ import com.jobtracker.mapper.ApplicationMapper; import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.service.ApplicationService; -import com.jobtracker.service.InterviewMetricsService; import com.jobtracker.util.SecurityUtils; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpTool.McpAnnotations; @@ -34,39 +33,18 @@ public class McpAnalyticsTools { private final ApplicationRepository applicationRepository; private final ApplicationMapper applicationMapper; private final ApplicationService applicationService; - private final InterviewMetricsService interviewMetricsService; private final SecurityUtils securityUtils; public McpAnalyticsTools(ApplicationRepository applicationRepository, ApplicationMapper applicationMapper, ApplicationService applicationService, - InterviewMetricsService interviewMetricsService, SecurityUtils securityUtils) { this.applicationRepository = applicationRepository; this.applicationMapper = applicationMapper; this.applicationService = applicationService; - this.interviewMetricsService = interviewMetricsService; this.securityUtils = securityUtils; } - @McpTool( - name = "Update-Interview-Count", - title = "Update Interview Count", - description = "Manually sets the cumulative interview count for the current user. Use this when the automatic counter missed interviews or needs correction.", - annotations = @McpAnnotations( - title = "Update Interview Count", - readOnlyHint = false, - destructiveHint = false, - idempotentHint = true, - openWorldHint = false)) - @Transactional - public String updateInterviewCount( - @McpToolParam(required = true, description = "The new total interview count (must be >= 0)") long count) { - UUID userId = securityUtils.getCurrentUserId(); - interviewMetricsService.setInterviewCount(userId, count); - return "Interview count updated to " + count; - } - @McpTool( name = "Get-Analytics", title = "Get Analytics", diff --git a/src/main/java/com/jobtracker/repository/ApplicationRepository.java b/src/main/java/com/jobtracker/repository/ApplicationRepository.java index 417b3df9..f2450d88 100644 --- a/src/main/java/com/jobtracker/repository/ApplicationRepository.java +++ b/src/main/java/com/jobtracker/repository/ApplicationRepository.java @@ -23,6 +23,9 @@ public interface ApplicationRepository extends JpaRepository { - User user = userRepository.getReferenceById(userId); - UserInterviewMetrics created = new UserInterviewMetrics(); - created.setUser(user); - created.setInterviewCount(0); - return created; - }); - metrics.setInterviewCount(count); - metricsRepository.save(metrics); - } - @Transactional(readOnly = true) public long getInterviewCount(UUID userId) { - return metricsRepository.findById(userId) - .map(UserInterviewMetrics::getInterviewCount) - .orElseGet(() -> eventRepository.countByUser_Id(userId)); - } - - private UserInterviewMetrics findOrCreateMetrics(User user) { - return metricsRepository.findByUser_Id(user.getId()) - .orElseGet(() -> { - UserInterviewMetrics created = new UserInterviewMetrics(); - created.setUser(user); - created.setInterviewCount(0); - return created; - }); + return applicationRepository.sumInterviewCountByUserId(userId); } } diff --git a/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql b/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql new file mode 100644 index 00000000..f5b5a738 --- /dev/null +++ b/src/main/resources/db/migration/V25__add_interview_count_to_job_applications.sql @@ -0,0 +1,2 @@ +ALTER TABLE job_applications + ADD COLUMN interview_count INT NOT NULL DEFAULT 0 AFTER interview_scheduled; From 9359c746c39a227386b450ad53634c05dada4709 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:13:07 +0000 Subject: [PATCH 3/5] fix: pass interviewCount null in McpApplicationTools ApplicationRequest calls https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../java/com/jobtracker/mcp/tools/McpApplicationTools.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java b/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java index 8e656d90..565d2549 100644 --- a/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java +++ b/src/main/java/com/jobtracker/mcp/tools/McpApplicationTools.java @@ -142,7 +142,8 @@ public ApplicationResponse createApplication( status, recruiterDmReminderEnabled != null ? recruiterDmReminderEnabled : Boolean.FALSE, note, - platform)); + platform, + null)); } @McpTool( @@ -181,7 +182,8 @@ public ApplicationResponse updateApplication( status, recruiterDmReminderEnabled != null ? recruiterDmReminderEnabled : Boolean.FALSE, note, - platform)); + platform, + null)); } @McpTool( From b2f6d115a91b5b14bd30230b05d914b3c090da5d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 16:47:04 +0000 Subject: [PATCH 4/5] fix: update tests broken by interviewCount field addition - Add missing interviewCount (null) arg to ApplicationRequest constructors in ApplicationControllerIT, ApplicationServiceTest, InterviewMetricsServiceTest - Add missing interviewCount (0) arg to ApplicationResponse constructors in ApplicationServiceTest, McpAnalyticsToolsTest, McpApplicationToolsTest - Rewrite InterviewMetricsServiceTest to use ApplicationRepository instead of the removed UserInterviewMetricsRepository https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../integration/ApplicationControllerIT.java | 8 ++-- .../unit/ApplicationServiceTest.java | 5 ++- .../unit/InterviewMetricsServiceTest.java | 38 +++++-------------- .../unit/mcp/McpAnalyticsToolsTest.java | 2 +- .../unit/mcp/McpApplicationToolsTest.java | 2 +- 5 files changed, 18 insertions(+), 37 deletions(-) diff --git a/src/test/java/com/jobtracker/integration/ApplicationControllerIT.java b/src/test/java/com/jobtracker/integration/ApplicationControllerIT.java index 4b4f9e5a..c1703b5d 100644 --- a/src/test/java/com/jobtracker/integration/ApplicationControllerIT.java +++ b/src/test/java/com/jobtracker/integration/ApplicationControllerIT.java @@ -294,7 +294,7 @@ void applicationLifecycle_shouldAutoAwardXpAndAvoidDuplicates() throws Exception ApplicationRequest createRequest = new ApplicationRequest( "Gamified App", "Recruiter", "Org", "https://example.com/job", LocalDate.now(), - false, false, null, "RH", false, null, null + false, false, null, "RH", false, null, null, null ); MvcResult createResult = mockMvc.perform(post("/api/v1/applications") @@ -339,7 +339,7 @@ void applicationLifecycle_shouldAutoAwardXpAndAvoidDuplicates() throws Exception ApplicationRequest addNoteRequest = new ApplicationRequest( "Gamified App", "Recruiter", "Org", "https://example.com/job", LocalDate.now(), - false, false, null, "Teste Técnico", false, "First note", null + false, false, null, "Teste Técnico", false, "First note", null, null ); mockMvc.perform(put("/api/v1/applications/{id}", id) @@ -352,7 +352,7 @@ void applicationLifecycle_shouldAutoAwardXpAndAvoidDuplicates() throws Exception ApplicationRequest updateNoteRequest = new ApplicationRequest( "Gamified App", "Recruiter", "Org", "https://example.com/job", LocalDate.now(), - false, false, null, "Teste Técnico", false, "Edited note", null + false, false, null, "Teste Técnico", false, "Edited note", null, null ); mockMvc.perform(put("/api/v1/applications/{id}", id) @@ -382,7 +382,7 @@ private ApplicationRequest buildRequest(String vacancyName) { return new ApplicationRequest( vacancyName, "Some Recruiter", "HR Department", "https://example.com/job", LocalDate.now().minusDays(1), - false, false, null, "RH", false, "Remember to follow up", null + false, false, null, "RH", false, "Remember to follow up", null, null ); } } diff --git a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java index 3ec79bca..2bae2923 100644 --- a/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java +++ b/src/test/java/com/jobtracker/unit/ApplicationServiceTest.java @@ -179,6 +179,7 @@ void update_shouldClearApplicationDate_whenMarkedToSendLater() { null, false, "Follow up this week", + null, null ); when(securityUtils.getCurrentUserId()).thenReturn(USER_UUID); @@ -351,7 +352,7 @@ private ApplicationRequest buildRequest() { return new ApplicationRequest( "Software Engineer", "Recruiter", "HR", "https://example.com/job", LocalDate.now(), - false, false, null, "RH", false, "Follow up this week", null + false, false, null, "RH", false, "Follow up this week", null, null ); } @@ -359,6 +360,6 @@ private ApplicationResponse buildApplicationResponse(UUID id) { return new ApplicationResponse(id, "Software Engineer", "Recruiter", "HR", "https://example.com/job", LocalDate.now(), false, false, null, "RH", null, false, LocalDateTime.now(), "Follow up this week", null, false, null, null, null, null, null, null, - LocalDateTime.now(), LocalDateTime.now()); + 0, LocalDateTime.now(), LocalDateTime.now()); } } diff --git a/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java b/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java index e8fd7391..a10159e1 100644 --- a/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java +++ b/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java @@ -2,32 +2,28 @@ import com.jobtracker.entity.JobApplication; import com.jobtracker.entity.User; -import com.jobtracker.entity.UserInterviewMetrics; import com.jobtracker.entity.enums.ApplicationStatus; +import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.repository.InterviewEventRepository; -import com.jobtracker.repository.UserInterviewMetricsRepository; import com.jobtracker.service.InterviewMetricsService; 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.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.Optional; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class InterviewMetricsServiceTest { @Mock - private UserInterviewMetricsRepository metricsRepository; + private ApplicationRepository applicationRepository; @Mock private InterviewEventRepository eventRepository; @@ -35,11 +31,10 @@ class InterviewMetricsServiceTest { private InterviewMetricsService service; private User user; private JobApplication application; - private UserInterviewMetrics metrics; @BeforeEach void setUp() { - service = new InterviewMetricsService(metricsRepository, eventRepository); + service = new InterviewMetricsService(applicationRepository, eventRepository); user = new User(); user.setId(UUID.fromString("00000000-0000-0000-0000-000000000001")); @@ -47,10 +42,6 @@ void setUp() { application = new JobApplication(); application.setId(UUID.fromString("00000000-0000-0000-0000-000000000002")); application.setUser(user); - - metrics = new UserInterviewMetrics(); - metrics.setUser(user); - metrics.setInterviewCount(2); } @Test @@ -72,35 +63,24 @@ void wasInterviewTriggered_shouldOnlyDetectEntryIntoInterviewStatus() { } @Test - void recordStatusTransition_shouldIncrementAndLogWhenEnteringInterviewStatus() { - when(metricsRepository.findByUser_Id(user.getId())).thenReturn(Optional.of(metrics)); - + void recordStatusTransition_shouldLogEventWhenEnteringInterviewStatus() { service.recordStatusTransition(application, ApplicationStatus.RH, ApplicationStatus.TESTE_TECNICO); - assertThat(metrics.getInterviewCount()).isEqualTo(3); - verify(metricsRepository).save(metrics); verify(eventRepository).save(any()); } @Test - void recordStatusTransition_shouldCreateMetricsWhenMissing() { - when(metricsRepository.findByUser_Id(user.getId())).thenReturn(Optional.empty()); - when(metricsRepository.save(any(UserInterviewMetrics.class))).thenAnswer(invocation -> invocation.getArgument(0)); - - service.recordStatusTransition(application, ApplicationStatus.RH, ApplicationStatus.TESTE_TECNICO); + void recordStatusTransition_shouldNotLogWhenStayingWithinInterviewStatuses() { + service.recordStatusTransition(application, ApplicationStatus.TESTE_TECNICO, ApplicationStatus.RH_NEGOCIACAO); - ArgumentCaptor metricsCaptor = ArgumentCaptor.forClass(UserInterviewMetrics.class); - verify(metricsRepository).save(metricsCaptor.capture()); - assertThat(metricsCaptor.getAllValues().getLast().getInterviewCount()).isEqualTo(1); - verify(eventRepository).save(any()); + verify(eventRepository, never()).save(any()); } @Test - void recordStatusTransition_shouldNotDoubleCountRepeatedSavesOrInterviewFlowMoves() { + void recordStatusTransition_shouldNotLogForNonInterviewTransitions() { service.recordStatusTransition(application, ApplicationStatus.TESTE_TECNICO, ApplicationStatus.TESTE_TECNICO); - service.recordStatusTransition(application, ApplicationStatus.TESTE_TECNICO, ApplicationStatus.RH_NEGOCIACAO); + service.recordStatusTransition(application, ApplicationStatus.RH, ApplicationStatus.REJEITADO); - verify(metricsRepository, never()).save(any()); verify(eventRepository, never()).save(any()); } } diff --git a/src/test/java/com/jobtracker/unit/mcp/McpAnalyticsToolsTest.java b/src/test/java/com/jobtracker/unit/mcp/McpAnalyticsToolsTest.java index a932ddc1..72d3cd90 100644 --- a/src/test/java/com/jobtracker/unit/mcp/McpAnalyticsToolsTest.java +++ b/src/test/java/com/jobtracker/unit/mcp/McpAnalyticsToolsTest.java @@ -238,6 +238,6 @@ private static ApplicationResponse applicationResponseWithId(UUID id) { null, false, null, null, null, null, null, null, - null, null); + 0, null, null); } } diff --git a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java index 7432dd77..b40dfcc3 100644 --- a/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java +++ b/src/test/java/com/jobtracker/unit/mcp/McpApplicationToolsTest.java @@ -220,6 +220,6 @@ private static ApplicationResponse applicationResponseWithId(UUID id) { null, false, null, null, null, null, null, null, - null, null); + 0, null, null); } } From 22bb34dcd046ed7021023d0eba6f23e2947a7da5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 5 Jun 2026 17:14:11 +0000 Subject: [PATCH 5/5] fix: restore auto-increment of interviewCount on interview status transition recordStatusTransition now increments application.interviewCount when entering an interview status, restoring the auto-count behavior. The manual field on the form allows correction when auto-count misses multiple rounds. https://claude.ai/code/session_015kfgrc2oHA881RT3bEYHZ6 --- .../com/jobtracker/service/InterviewMetricsService.java | 2 ++ .../com/jobtracker/unit/InterviewMetricsServiceTest.java | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jobtracker/service/InterviewMetricsService.java b/src/main/java/com/jobtracker/service/InterviewMetricsService.java index 8c07df23..15dfe907 100644 --- a/src/main/java/com/jobtracker/service/InterviewMetricsService.java +++ b/src/main/java/com/jobtracker/service/InterviewMetricsService.java @@ -61,6 +61,8 @@ public void recordStatusTransition(JobApplication application, return; } + application.setInterviewCount(application.getInterviewCount() + 1); + InterviewEvent event = new InterviewEvent(); event.setUser(application.getUser()); event.setApplication(application); diff --git a/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java b/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java index a10159e1..41e4f26d 100644 --- a/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java +++ b/src/test/java/com/jobtracker/unit/InterviewMetricsServiceTest.java @@ -63,24 +63,27 @@ void wasInterviewTriggered_shouldOnlyDetectEntryIntoInterviewStatus() { } @Test - void recordStatusTransition_shouldLogEventWhenEnteringInterviewStatus() { + void recordStatusTransition_shouldIncrementCountAndLogEventWhenEnteringInterviewStatus() { service.recordStatusTransition(application, ApplicationStatus.RH, ApplicationStatus.TESTE_TECNICO); + assertThat(application.getInterviewCount()).isEqualTo(1); verify(eventRepository).save(any()); } @Test - void recordStatusTransition_shouldNotLogWhenStayingWithinInterviewStatuses() { + void recordStatusTransition_shouldNotIncrementWhenStayingWithinInterviewStatuses() { service.recordStatusTransition(application, ApplicationStatus.TESTE_TECNICO, ApplicationStatus.RH_NEGOCIACAO); + assertThat(application.getInterviewCount()).isEqualTo(0); verify(eventRepository, never()).save(any()); } @Test - void recordStatusTransition_shouldNotLogForNonInterviewTransitions() { + void recordStatusTransition_shouldNotIncrementForNonInterviewTransitions() { service.recordStatusTransition(application, ApplicationStatus.TESTE_TECNICO, ApplicationStatus.TESTE_TECNICO); service.recordStatusTransition(application, ApplicationStatus.RH, ApplicationStatus.REJEITADO); + assertThat(application.getInterviewCount()).isEqualTo(0); verify(eventRepository, never()).save(any()); } }