Skip to content

Commit 8240ea6

Browse files
committed
feat: update the emailSender, service and controller
1 parent 7f0782d commit 8240ea6

4 files changed

Lines changed: 170 additions & 28 deletions

File tree

src/main/java/com/itasocialacademy/oitassist/participation/components/sender/AsyncEmailSender.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.itasocialacademy.oitassist.core.properties.WebClientProperties;
44
import com.itasocialacademy.oitassist.core.service.interfaces.EmailService;
5+
import com.itasocialacademy.oitassist.participation.dao.dto.event.ApplicationDecisionListEvent;
56
import com.itasocialacademy.oitassist.participation.dao.dto.event.ApplicationDecisionEvent;
67
import com.itasocialacademy.oitassist.participation.dao.dto.event.InvitationRequestEvent;
78
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
@@ -50,11 +51,7 @@ public void sendDecisionEmail(ApplicationDecisionEvent event) {
5051
String profileLink = buildLink(PROFILE_PATH);
5152
log.info("Handling ApplicationDecisionEvent for email={}, status={}", email, status);
5253

53-
String template = switch (status) {
54-
case ACCEPTED -> "application-accepted.html";
55-
case REJECTED -> "application-rejected.html";
56-
default -> throw new IllegalArgumentException("No email template for status: " + status);
57-
};
54+
String template = determineTemplate(status);
5855
Map<String, String> extraParams = new HashMap<>();
5956
if (status == RequestStatus.REJECTED) {
6057
if (event.rejectionReason() != null && !event.rejectionReason().isBlank()) {
@@ -105,11 +102,49 @@ public void sendInvitationEmail(InvitationRequestEvent event) {
105102
}
106103
}
107104

105+
@Async
106+
public void sendDecisionEmailList(ApplicationDecisionListEvent event) {
107+
log.info("Handling ApplicationDecisionListEvent for {} users", event.users().size());
108+
109+
String competitionLink = buildLink(COMPETITION_PATH);
110+
String profileLink = buildLink(PROFILE_PATH);
111+
RequestStatus status = event.status();
112+
String template = determineTemplate(status);
113+
Map<String, String> extraParams = new HashMap<>();
114+
if (status == RequestStatus.REJECTED) {
115+
if (event.rejectionReason() != null && !event.rejectionReason().isBlank()) {
116+
extraParams.put("rejectionReason", event.rejectionReason());
117+
}
118+
extraParams.put("profileLink", profileLink);
119+
}
120+
for (UserProfileDetails user : event.users()) {
121+
Map<String, String> root = new HashMap<>(Map.of(
122+
"firstName", user.firstName(),
123+
"competitionTitle", event.competitionTitle(),
124+
"stageTitle", event.stageTitle(),
125+
"competitionLink", competitionLink));
126+
root.putAll(extraParams);
127+
emailService.sendTemplateEmail(
128+
user.email(),
129+
template,
130+
"Статус заявки",
131+
root);
132+
}
133+
}
134+
108135
private String buildLink(String link) {
109136
return UriComponentsBuilder
110137
.fromUriString(webClientProperties.origin())
111138
.path(link)
112139
.build()
113140
.toUriString();
114141
}
142+
143+
private String determineTemplate(RequestStatus status) {
144+
return switch (status) {
145+
case ACCEPTED -> "application-accepted.html";
146+
case REJECTED -> "application-rejected.html";
147+
default -> throw new IllegalArgumentException("No email template for status: " + status);
148+
};
149+
}
115150
}

src/main/java/com/itasocialacademy/oitassist/participation/controller/ApplicationController.java

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
package com.itasocialacademy.oitassist.participation.controller;
22

33
import com.itasocialacademy.oitassist.core.dao.dto.response.PageResponse;
4+
import com.itasocialacademy.oitassist.participation.dao.dto.request.AcceptApplicationListRequest;
5+
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectApplicationListRequest;
46
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectEnrollmentRequest;
5-
import com.itasocialacademy.oitassist.participation.dao.dto.response.ApplicationListItemResponse;
6-
import com.itasocialacademy.oitassist.participation.dao.dto.response.CreateApplicationResponse;
7-
import com.itasocialacademy.oitassist.participation.dao.dto.response.ProcessApplicationResponse;
7+
import com.itasocialacademy.oitassist.participation.dao.dto.response.*;
88
import com.itasocialacademy.oitassist.participation.service.interfaces.ApplicationService;
99
import io.swagger.v3.oas.annotations.Operation;
1010
import io.swagger.v3.oas.annotations.media.Content;
1111
import io.swagger.v3.oas.annotations.media.Schema;
1212
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1313
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1414
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import jakarta.validation.Valid;
1516
import lombok.RequiredArgsConstructor;
1617
import org.springdoc.core.annotations.ParameterObject;
1718
import org.springframework.data.domain.Pageable;
@@ -84,9 +85,39 @@ public ResponseEntity<ProcessApplicationResponse> acceptRequest(@PathVariable Lo
8485
.body((ProcessApplicationResponse) applicationService.acceptRequest(id));
8586
}
8687

88+
@Operation(
89+
summary = "Accept users' applications",
90+
description = "Accepts the application list and creates Participation records.")
91+
@ApiResponses(value = {
92+
@ApiResponse(responseCode = "201", description = """
93+
The request was processed. Note that a 201 response does not guarantee \s
94+
every application was successfully accepted - check the `succeeded` and `failed` \s
95+
fields in the response body for the per-student outcome (possible reasons: \s
96+
application not found, the request's status is not PENDING, applicant is already a participant).
97+
""",
98+
content = @Content(mediaType = "application/json",
99+
schema = @Schema(implementation = AcceptedApplicationListResponse.class))),
100+
@ApiResponse(responseCode = "400", description = """
101+
Could not accept applications. Possible reasons:\s
102+
- The request contains duplicate application IDs.\s
103+
- Applications don't belong to the same competition stage.""",
104+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
105+
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
106+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
107+
@ApiResponse(responseCode = "404", description = "No requested application was found",
108+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
109+
})
110+
@PreAuthorize("hasRole('ORG')")
111+
@PostMapping("/enrollment/applications/accept/list")
112+
public ResponseEntity<AcceptedApplicationListResponse> acceptRequests(
113+
@Valid @RequestBody AcceptApplicationListRequest request) {
114+
return ResponseEntity.status(HttpStatus.CREATED)
115+
.body(applicationService.acceptApplicationList(request));
116+
}
117+
87118
@Operation(
88119
summary = "Reject user's application",
89-
description = "Rejects the application with provided rejection reason. ")
120+
description = "Rejects the application with provided rejection reason.")
90121
@ApiResponses(value = {
91122
@ApiResponse(responseCode = "200", description = "Application rejected successfully",
92123
content = @Content(mediaType = "application/json",
@@ -107,6 +138,36 @@ public ResponseEntity<ProcessApplicationResponse> rejectRequest(
107138
.body((ProcessApplicationResponse) applicationService.rejectRequest(id, rejectEnrollmentRequest));
108139
}
109140

141+
@Operation(
142+
summary = "Reject users' applications",
143+
description = "Rejects the application list with provided rejection reason.")
144+
@ApiResponses(value = {
145+
@ApiResponse(responseCode = "201", description = """
146+
The request was processed. Note that a 201 response does not guarantee \s
147+
every application was successfully rejected - check the `succeeded` and `failed` \s
148+
fields in the response body for the per-student outcome (possible reasons: \s
149+
application not found, the request's status is not PENDING).
150+
""",
151+
content = @Content(mediaType = "application/json",
152+
schema = @Schema(implementation = RejectedApplicationListResponse.class))),
153+
@ApiResponse(responseCode = "400", description = """
154+
Could not reject applications. Possible reasons:\s
155+
- The request contains duplicate application IDs.\s
156+
- Applications don't belong to the same competition stage.""",
157+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
158+
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
159+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
160+
@ApiResponse(responseCode = "404", description = "No requested application was found",
161+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
162+
})
163+
@PreAuthorize("hasRole('ORG')")
164+
@PostMapping("/enrollment/applications/reject/list")
165+
public ResponseEntity<RejectedApplicationListResponse> rejectRequests(
166+
@Valid @RequestBody RejectApplicationListRequest request) {
167+
return ResponseEntity.status(HttpStatus.CREATED)
168+
.body(applicationService.rejectApplicationList(request));
169+
}
170+
110171
@Operation(
111172
summary = "Cancel user's application",
112173
description = "Cancels the application from a specific user. ")

src/main/java/com/itasocialacademy/oitassist/participation/service/ApplicationServiceImpl.java

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.itasocialacademy.oitassist.core.enums.ErrorCode;
1212
import com.itasocialacademy.oitassist.core.exceptions.AuthorizationException;
1313
import com.itasocialacademy.oitassist.participation.components.saver.ApplicationDecisionsSaver;
14+
import com.itasocialacademy.oitassist.participation.dao.dto.event.ApplicationDecisionListEvent;
1415
import com.itasocialacademy.oitassist.participation.dao.dto.event.ApplicationDecisionEvent;
1516
import com.itasocialacademy.oitassist.participation.dao.dto.request.AcceptApplicationListRequest;
1617
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectApplicationListRequest;
@@ -106,15 +107,16 @@ public ProcessApplicationResponse acceptRequest(Long applicationId) {
106107
}
107108

108109
@Override
109-
public AcceptedApplicationListResponse acceptApplications(
110-
AcceptApplicationListRequest request,
111-
Long competitionId,
112-
Long stageId) {
110+
public AcceptedApplicationListResponse acceptApplicationList(AcceptApplicationListRequest request) {
113111
Long userId = getCurrentUserIdOrThrow();
114112
List<Long> applicationIds = validateNoDuplicatesOrThrow(request.applicationIds());
115113

116114
List<Application> applications = applicationRepository.findAll(
117115
ApplicationSpecification.applicationIdIn(applicationIds));
116+
validateApplicationsBelongToTheSameCompetitionAndStage(applications);
117+
Long competitionId = applications.getFirst().getCompetitionId();
118+
Long stageId = applications.getFirst().getStageId();
119+
118120
List<SucceededApplicationAcceptingItemResponse> succeeded = new ArrayList<>();
119121
List<FailedApplicationDecisionItemResponse> failed = new ArrayList<>();
120122

@@ -152,7 +154,10 @@ public AcceptedApplicationListResponse acceptApplications(
152154
.build();
153155

154156
if (!succeeded.isEmpty()) {
155-
157+
scheduleAcceptedEmailList(
158+
competitionId,
159+
stageId,
160+
succeeded.stream().map(SucceededApplicationAcceptingItemResponse::participantId).toList());
156161
}
157162

158163
return response;
@@ -180,15 +185,16 @@ public ProcessApplicationResponse rejectRequest(Long applicationId, RejectEnroll
180185
}
181186

182187
@Override
183-
public RejectedApplicationListResponse rejectApplications(
184-
RejectApplicationListRequest request,
185-
Long competitionId,
186-
Long stageId) {
188+
public RejectedApplicationListResponse rejectApplicationList(RejectApplicationListRequest request) {
187189
Long userId = getCurrentUserIdOrThrow();
188190
List<Long> applicationIds = validateNoDuplicatesOrThrow(request.applicationIds());
189191

190192
List<Application> applications = applicationRepository.findAll(
191193
ApplicationSpecification.applicationIdIn(applicationIds));
194+
validateApplicationsBelongToTheSameCompetitionAndStage(applications);
195+
Long competitionId = applications.getFirst().getCompetitionId();
196+
Long stageId = applications.getFirst().getStageId();
197+
192198
List<SucceededApplicationRejectingItemResponse> succeeded = new ArrayList<>();
193199
List<FailedApplicationDecisionItemResponse> failed = new ArrayList<>();
194200

@@ -207,7 +213,7 @@ public RejectedApplicationListResponse rejectApplications(
207213
Application savedApplication = applicationSaver.saveRejectedApplication(
208214
userId, application, request.rejectionReason());
209215
succeeded.add(new SucceededApplicationRejectingItemResponse(
210-
savedApplication.getId(), RequestStatus.REJECTED));
216+
savedApplication.getId(), savedApplication.getUserId(), RequestStatus.REJECTED));
211217
} catch (DataIntegrityViolationException e) {
212218
throw new UnexpectedConstraintViolationException(
213219
"Unexpected database constraint violation while accepting application",
@@ -221,7 +227,11 @@ public RejectedApplicationListResponse rejectApplications(
221227
.build();
222228

223229
if (!succeeded.isEmpty()) {
224-
230+
scheduleRejectedEmailList(
231+
competitionId,
232+
stageId,
233+
succeeded.stream().map(SucceededApplicationRejectingItemResponse::studentId).toList(),
234+
request.rejectionReason());
225235
}
226236

227237
return response;
@@ -384,6 +394,15 @@ private void scheduleRejectedEmail(Long competitionId, Long stageId, Long userId
384394
scheduleDecisionEmailAfterCommit(competitionId, stageId, userId, rejectionReason, RequestStatus.REJECTED);
385395
}
386396

397+
private void scheduleAcceptedEmailList(Long competitionId, Long stageId, List<Long> userIds) {
398+
scheduleApplicationDecisionEmails(competitionId, stageId, userIds, null, RequestStatus.ACCEPTED);
399+
}
400+
401+
private void scheduleRejectedEmailList(
402+
Long competitionId, Long stageId, List<Long> userIds, String rejectionReason) {
403+
scheduleApplicationDecisionEmails(competitionId, stageId, userIds, rejectionReason, RequestStatus.REJECTED);
404+
}
405+
387406
private List<Long> validateNoDuplicatesOrThrow(List<Long> rawIds) {
388407
Set<Long> seen = new HashSet<>();
389408
Set<Long> duplicates = rawIds.stream()
@@ -401,4 +420,37 @@ private boolean isUniqueParticipationConstraint(DataIntegrityViolationException
401420
}
402421
return false;
403422
}
423+
424+
private void scheduleApplicationDecisionEmails(
425+
Long competitionId,
426+
Long stageId,
427+
List<Long> succeededIds,
428+
String rejectionReason,
429+
RequestStatus status) {
430+
String competitionTitle = getCompetitionInfoOrThrow(competitionId).title();
431+
String stageTitle = getStageInfoOrThrow(stageId).title();
432+
List<UserProfileDetails> users = userFacade.findProfilesByIds(succeededIds);
433+
434+
ApplicationDecisionListEvent event = new ApplicationDecisionListEvent(
435+
competitionTitle, stageTitle, users, rejectionReason, status);
436+
emailSender.sendDecisionEmailList(event);
437+
}
438+
439+
private void validateApplicationsBelongToTheSameCompetitionAndStage(List<Application> applications) {
440+
if (applications.isEmpty()) {
441+
throw new ApplicationNotFoundException("None of the requested applications were found");
442+
}
443+
444+
Long expectedCompetitionId = applications.getFirst().getCompetitionId();
445+
Long expectedStageId = applications.getFirst().getStageId();
446+
447+
boolean allMatch =
448+
applications.stream().allMatch(app -> Objects.equals(app.getCompetitionId(), expectedCompetitionId)
449+
&& Objects.equals(app.getStageId(), expectedStageId));
450+
451+
if (!allMatch) {
452+
throw new UnableToProcessApplicationException(
453+
"The applications don't belong to the same competition stage");
454+
}
455+
}
404456
}

src/main/java/com/itasocialacademy/oitassist/participation/service/interfaces/ApplicationService.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@
1010
public interface ApplicationService extends EnrollmentService<ApplicationListItemResponse> {
1111
EnrollmentResponse sendApplicationRequest(Long competitionId, Long stageId);
1212

13-
AcceptedApplicationListResponse acceptApplications(
14-
AcceptApplicationListRequest request,
15-
Long competitionId,
16-
Long stageId);
13+
AcceptedApplicationListResponse acceptApplicationList(AcceptApplicationListRequest request);
1714

18-
RejectedApplicationListResponse rejectApplications(
19-
RejectApplicationListRequest request,
20-
Long competitionId,
21-
Long stageId);
15+
RejectedApplicationListResponse rejectApplicationList(RejectApplicationListRequest request);
2216
}

0 commit comments

Comments
 (0)