Skip to content

Commit 5d9c316

Browse files
Merge pull request #53 from vimal-tech-starter/feature/operational-admin-controls-phase-11.8
Feat: operational email retry and Admin controls
2 parents 181e591 + 81ab438 commit 5d9c316

13 files changed

Lines changed: 450 additions & 15 deletions

src/main/java/com/vimaltech/contactapi/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
2626
.requestMatchers(HttpMethod.GET, "/api/v1/contacts/options").permitAll()
2727

2828
// 🔒 Admin endpoint
29-
.requestMatchers(HttpMethod.GET, "/api/v1/contacts").authenticated()
29+
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
30+
.requestMatchers(HttpMethod.GET, "/api/v1/contacts").hasRole("ADMIN")
3031

3132
// Everything else
3233
.anyRequest().permitAll()
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.vimaltech.contactapi.controller.admin;
2+
3+
import com.vimaltech.contactapi.dto.admin.EmailLogResponse;
4+
import com.vimaltech.contactapi.dto.admin.PagedResponse;
5+
import com.vimaltech.contactapi.dto.admin.RetryOperationResponse;
6+
import com.vimaltech.contactapi.dto.admin.RetryStatisticsResponse;
7+
import com.vimaltech.contactapi.enums.EmailStatus;
8+
import com.vimaltech.contactapi.service.admin.EmailOperationsService;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.data.domain.Page;
11+
import org.springframework.data.domain.PageRequest;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
@RestController
17+
@RequestMapping("/api/v1/admin/email-retries")
18+
@RequiredArgsConstructor
19+
public class EmailRetryAdminController {
20+
21+
private final EmailOperationsService emailOperationsService;
22+
23+
@GetMapping("/stats")
24+
public ResponseEntity<RetryStatisticsResponse> getStatistics() {
25+
26+
return ResponseEntity.ok(
27+
emailOperationsService.getStatistics()
28+
);
29+
}
30+
31+
@GetMapping
32+
public ResponseEntity<PagedResponse<EmailLogResponse>> getEmailLogs(
33+
34+
@RequestParam(required = false)
35+
EmailStatus status,
36+
37+
@RequestParam(defaultValue = "0")
38+
int page,
39+
40+
@RequestParam(defaultValue = "20")
41+
int size
42+
) {
43+
44+
Pageable pageable = PageRequest.of(page, size);
45+
46+
Page<EmailLogResponse> emailPage =
47+
emailOperationsService.getEmailLogs(
48+
status,
49+
pageable
50+
);
51+
52+
PagedResponse<EmailLogResponse> response =
53+
new PagedResponse<>(
54+
emailPage.getContent(),
55+
emailPage.getNumber(),
56+
emailPage.getSize(),
57+
emailPage.getTotalElements(),
58+
emailPage.getTotalPages(),
59+
emailPage.isFirst(),
60+
emailPage.isLast()
61+
);
62+
63+
return ResponseEntity.ok(response);
64+
}
65+
66+
@GetMapping("/{id}")
67+
public ResponseEntity<EmailLogResponse> getEmailLog(
68+
@PathVariable Long id
69+
) {
70+
71+
return ResponseEntity.ok(
72+
emailOperationsService.getEmailLog(id)
73+
);
74+
}
75+
76+
@PostMapping("/{id}/retry")
77+
public ResponseEntity<RetryOperationResponse> retryEmail(
78+
@PathVariable Long id
79+
) {
80+
81+
return ResponseEntity.ok(
82+
emailOperationsService.retryEmail(id)
83+
);
84+
}
85+
86+
@PostMapping("/retry-all")
87+
public ResponseEntity<RetryOperationResponse> retryAllFailedEmails() {
88+
89+
return ResponseEntity.ok(
90+
emailOperationsService.retryAllFailedEmails()
91+
);
92+
}
93+
94+
@PostMapping("/{id}/reset")
95+
public ResponseEntity<RetryOperationResponse> resetPermanentFailure(
96+
@PathVariable Long id
97+
) {
98+
99+
return ResponseEntity.ok(
100+
emailOperationsService.resetPermanentFailure(id)
101+
);
102+
}
103+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.vimaltech.contactapi.dto.admin;
2+
3+
import com.vimaltech.contactapi.enums.EmailStatus;
4+
5+
import java.time.LocalDateTime;
6+
7+
public record EmailLogResponse(
8+
9+
Long id,
10+
11+
String toEmail,
12+
13+
String subject,
14+
15+
EmailStatus status,
16+
17+
int retryCount,
18+
19+
String lastError,
20+
21+
LocalDateTime createdAt,
22+
23+
LocalDateTime lastAttemptAt
24+
) {
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.vimaltech.contactapi.dto.admin;
2+
3+
import java.util.List;
4+
5+
public record PagedResponse<T>(
6+
7+
List<T> content,
8+
9+
int page,
10+
11+
int size,
12+
13+
long totalElements,
14+
15+
int totalPages,
16+
17+
boolean first,
18+
19+
boolean last
20+
) {
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.vimaltech.contactapi.dto.admin;
2+
3+
import java.time.LocalDateTime;
4+
5+
public record RetryOperationResponse(
6+
7+
boolean success,
8+
9+
String message,
10+
11+
Long emailId,
12+
13+
LocalDateTime timestamp
14+
) {
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.vimaltech.contactapi.dto.admin;
2+
3+
public record RetryStatisticsResponse(
4+
5+
long pending,
6+
7+
long inProgress,
8+
9+
long sent,
10+
11+
long failed,
12+
13+
long failedPermanent
14+
) {
15+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.vimaltech.contactapi.exception;
2+
3+
public class EmailNotFoundException extends RuntimeException {
4+
5+
public EmailNotFoundException(Long id) {
6+
super("Email log not found with id: " + id);
7+
}
8+
}

src/main/java/com/vimaltech/contactapi/exception/GlobalExceptionHandler.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
import org.springframework.http.ResponseEntity;
88
import org.springframework.http.converter.HttpMessageNotReadableException;
99
import org.springframework.web.bind.MethodArgumentNotValidException;
10-
import org.springframework.web.bind.annotation.*;
10+
import org.springframework.web.bind.annotation.ExceptionHandler;
11+
import org.springframework.web.bind.annotation.RestControllerAdvice;
1112

1213
import java.time.LocalDateTime;
1314

1415
@RestControllerAdvice
1516
public class GlobalExceptionHandler {
1617

17-
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
18+
private static final Logger log =
19+
LoggerFactory.getLogger(GlobalExceptionHandler.class);
1820

1921
// ✅ 1. Bean validation errors (@Valid)
2022
@ExceptionHandler(MethodArgumentNotValidException.class)
@@ -39,22 +41,25 @@ public ResponseEntity<ApiResponse> handleValidationException(
3941
return ResponseEntity.badRequest().body(response);
4042
}
4143

42-
// ✅ 2. JSON parsing / ENUM errors (UPDATED - precise handling)
44+
// ✅ 2. JSON parsing / ENUM errors
4345
@ExceptionHandler(HttpMessageNotReadableException.class)
44-
public ResponseEntity<ApiResponse> handleJsonParseError(HttpMessageNotReadableException ex) {
46+
public ResponseEntity<ApiResponse> handleJsonParseError(
47+
HttpMessageNotReadableException ex) {
4548

46-
log.warn("Invalid request format or enum value: {}", ex.getMessage());
49+
log.warn(
50+
"Invalid request format or enum value: {}",
51+
ex.getMessage()
52+
);
4753

4854
String message = "Invalid request format";
4955

50-
// 🔥 Extract root cause (THIS is what you asked about)
5156
Throwable root = ex.getCause();
5257

53-
if (root instanceof IllegalArgumentException &&
54-
root.getMessage() != null &&
55-
root.getMessage().contains("Invalid subject value")) {
58+
if (root instanceof IllegalArgumentException
59+
&& root.getMessage() != null
60+
&& root.getMessage().contains("Invalid subject value")) {
5661

57-
message = root.getMessage(); // use exact enum error
62+
message = root.getMessage();
5863
}
5964

6065
ApiResponse response = new ApiResponse(
@@ -66,9 +71,27 @@ public ResponseEntity<ApiResponse> handleJsonParseError(HttpMessageNotReadableEx
6671
return ResponseEntity.badRequest().body(response);
6772
}
6873

69-
// ✅ 3. Catch-all (fallback)
74+
// ✅ 3. Email log not found
75+
@ExceptionHandler(EmailNotFoundException.class)
76+
public ResponseEntity<ApiResponse> handleEmailNotFoundException(
77+
EmailNotFoundException ex) {
78+
79+
log.warn("Email log not found: {}", ex.getMessage());
80+
81+
ApiResponse response = new ApiResponse(
82+
false,
83+
ex.getMessage(),
84+
LocalDateTime.now()
85+
);
86+
87+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
88+
.body(response);
89+
}
90+
91+
// ✅ 4. Catch-all (fallback)
7092
@ExceptionHandler(Exception.class)
71-
public ResponseEntity<ApiResponse> handleGenericException(Exception ex) {
93+
public ResponseEntity<ApiResponse> handleGenericException(
94+
Exception ex) {
7295

7396
log.error("Unhandled exception occurred", ex);
7497

@@ -78,6 +101,8 @@ public ResponseEntity<ApiResponse> handleGenericException(Exception ex) {
78101
LocalDateTime.now()
79102
);
80103

81-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
104+
return ResponseEntity.status(
105+
HttpStatus.INTERNAL_SERVER_ERROR
106+
).body(response);
82107
}
83108
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.vimaltech.contactapi.mapper;
2+
3+
import com.vimaltech.contactapi.dto.admin.EmailLogResponse;
4+
import com.vimaltech.contactapi.entity.EmailLog;
5+
6+
public final class EmailLogMapper {
7+
8+
private EmailLogMapper() {
9+
}
10+
11+
public static EmailLogResponse toResponse(
12+
EmailLog emailLog
13+
) {
14+
15+
return new EmailLogResponse(
16+
emailLog.getId(),
17+
emailLog.getToEmail(),
18+
emailLog.getSubject(),
19+
emailLog.getStatus(),
20+
emailLog.getRetryCount(),
21+
emailLog.getLastError(),
22+
emailLog.getCreatedAt(),
23+
emailLog.getLastAttemptAt()
24+
);
25+
}
26+
}

src/main/java/com/vimaltech/contactapi/repository/EmailLogRepository.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import com.vimaltech.contactapi.entity.EmailLog;
44
import com.vimaltech.contactapi.enums.EmailStatus;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
57
import org.springframework.data.jpa.repository.JpaRepository;
68
import org.springframework.stereotype.Repository;
79

@@ -14,4 +16,17 @@ public interface EmailLogRepository
1416
List<EmailLog> findTop10ByStatusOrderByCreatedAtAsc(
1517
EmailStatus status
1618
);
19+
20+
Page<EmailLog> findByStatus(
21+
EmailStatus status,
22+
Pageable pageable
23+
);
24+
25+
List<EmailLog> findByStatus(
26+
EmailStatus status
27+
);
28+
29+
long countByStatus(
30+
EmailStatus status
31+
);
1732
}

0 commit comments

Comments
 (0)