Skip to content

Commit c997ef3

Browse files
authored
[Feature] implement getting pending enrollment requests (#570)
1 parent a3b1d69 commit c997ef3

24 files changed

Lines changed: 866 additions & 73 deletions

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.itasocialacademy.oitassist.participation.controller;
22

3+
import com.itasocialacademy.oitassist.core.dao.dto.response.PageResponse;
34
import com.itasocialacademy.oitassist.participation.dao.dto.request.CreateApplicationRequest;
5+
import com.itasocialacademy.oitassist.participation.dao.dto.request.EnrollmentRequestsFilter;
46
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectEnrollmentRequest;
7+
import com.itasocialacademy.oitassist.participation.dao.dto.response.ApplicationListItemResponse;
58
import com.itasocialacademy.oitassist.participation.dao.dto.response.CreateApplicationResponse;
69
import com.itasocialacademy.oitassist.participation.dao.dto.response.ProcessApplicationResponse;
710
import com.itasocialacademy.oitassist.participation.service.interfaces.ApplicationService;
@@ -11,8 +14,10 @@
1114
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1215
import io.swagger.v3.oas.annotations.responses.ApiResponses;
1316
import io.swagger.v3.oas.annotations.tags.Tag;
14-
import jakarta.validation.Valid;
1517
import lombok.RequiredArgsConstructor;
18+
import org.springdoc.core.annotations.ParameterObject;
19+
import org.springframework.data.domain.Pageable;
20+
import org.springframework.data.web.PageableDefault;
1621
import org.springframework.http.HttpStatus;
1722
import org.springframework.http.ResponseEntity;
1823
import org.springframework.security.access.prepost.PreAuthorize;
@@ -21,7 +26,7 @@
2126

2227
@RestController
2328
@RequiredArgsConstructor
24-
@RequestMapping("/api/v1/competitions/applications")
29+
@RequestMapping("/api/v1")
2530
@Tag(name = "Application Manager v1", description = "Operations related to competition applications")
2631
public class ApplicationController {
2732
private final ApplicationService applicationService;
@@ -51,11 +56,14 @@ public class ApplicationController {
5156
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
5257
})
5358
@PreAuthorize("hasRole('USER')")
54-
@PostMapping
59+
@PostMapping("/competitions/{competitionId}/stages/{stageId}/applications")
5560
public ResponseEntity<CreateApplicationResponse> apply(
56-
@Valid @RequestBody CreateApplicationRequest createApplicationRequest) {
61+
@PathVariable Long competitionId,
62+
@PathVariable Long stageId) {
63+
CreateApplicationRequest request = CreateApplicationRequest.builder()
64+
.competitionId(competitionId).stageId(stageId).build();
5765
return ResponseEntity.status(HttpStatus.CREATED)
58-
.body((CreateApplicationResponse) applicationService.sendEnrollmentRequest(createApplicationRequest));
66+
.body((CreateApplicationResponse) applicationService.sendEnrollmentRequest(request));
5967
}
6068

6169
@Operation(
@@ -74,7 +82,7 @@ public ResponseEntity<CreateApplicationResponse> apply(
7482
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
7583
})
7684
@PreAuthorize("hasRole('ORG')")
77-
@PostMapping("/accept/{id}")
85+
@PostMapping("/enrollment/applications/{id}/accept")
7886
public ResponseEntity<ProcessApplicationResponse> acceptRequest(@PathVariable Long id) {
7987
return ResponseEntity.status(HttpStatus.CREATED)
8088
.body((ProcessApplicationResponse) applicationService.acceptRequest(id));
@@ -95,7 +103,7 @@ public ResponseEntity<ProcessApplicationResponse> acceptRequest(@PathVariable Lo
95103
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
96104
})
97105
@PreAuthorize("hasRole('ORG')")
98-
@PatchMapping("/reject/{id}")
106+
@PatchMapping("/enrollment/applications/{id}/reject")
99107
public ResponseEntity<ProcessApplicationResponse> rejectRequest(
100108
@PathVariable Long id,
101109
@RequestBody RejectEnrollmentRequest rejectEnrollmentRequest) {
@@ -111,7 +119,7 @@ public ResponseEntity<ProcessApplicationResponse> rejectRequest(
111119
content = @Content(mediaType = "application/json",
112120
schema = @Schema(implementation = ProcessApplicationResponse.class))),
113121
@ApiResponse(responseCode = "400", description = """
114-
User is unable to apply. Possible reasons:\s
122+
User is unable to cancel the request. Possible reasons:\s
115123
- The user is not the owner of the application.\s
116124
- The application request's status is not PENDING.""",
117125
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@@ -121,9 +129,42 @@ public ResponseEntity<ProcessApplicationResponse> rejectRequest(
121129
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
122130
})
123131
@PreAuthorize("hasRole('USER')")
124-
@PatchMapping("/cancel/{id}")
132+
@PatchMapping("/enrollment/applications/{id}/cancel")
125133
public ResponseEntity<ProcessApplicationResponse> cancelRequest(@PathVariable Long id) {
126134
return ResponseEntity.status(HttpStatus.OK)
127135
.body((ProcessApplicationResponse) applicationService.cancelRequest(id));
128136
}
137+
138+
@Operation(
139+
summary = "Get users' application requests",
140+
description = "Retrieves a paginated list of application requests "
141+
+ "for a specific competition stage.")
142+
@ApiResponses(value = {
143+
@ApiResponse(responseCode = "200", description = "Applications retrieved successfully",
144+
content = @Content(mediaType = "application/json",
145+
schema = @Schema(implementation = PageResponse.class))),
146+
@ApiResponse(responseCode = "400", description = """
147+
The competition and stage info error. The reason: \s
148+
specified stage ID does not belong to the competition ID.""",
149+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
150+
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
151+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
152+
@ApiResponse(responseCode = "404", description = """
153+
Resource missing. Possible reasons:\s
154+
- The requested competition does not exist.\s
155+
- The requested stage does not exist.""",
156+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
157+
})
158+
@PreAuthorize("hasRole('ORG')")
159+
@GetMapping("/competitions/{competitionId}/stages/{stageId}/applications")
160+
public ResponseEntity<PageResponse<ApplicationListItemResponse>> getRequests(
161+
@PathVariable Long competitionId,
162+
@PathVariable Long stageId,
163+
@RequestParam(required = false) String search,
164+
@ParameterObject @PageableDefault(size = 20, sort = "issuedAt") Pageable pageable) {
165+
EnrollmentRequestsFilter request = EnrollmentRequestsFilter.builder()
166+
.competitionId(competitionId).stageId(stageId).build();
167+
return ResponseEntity.status(HttpStatus.OK)
168+
.body(PageResponse.from(applicationService.getEnrollmentRequests(request, search, pageable)));
169+
}
129170
}

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

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package com.itasocialacademy.oitassist.participation.controller;
22

3+
import com.itasocialacademy.oitassist.core.dao.dto.response.PageResponse;
34
import com.itasocialacademy.oitassist.participation.dao.dto.request.CreateInvitationRequest;
5+
import com.itasocialacademy.oitassist.participation.dao.dto.request.EnrollmentRequestsFilter;
46
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectEnrollmentRequest;
57
import com.itasocialacademy.oitassist.participation.dao.dto.response.CreateInvitationResponse;
8+
import com.itasocialacademy.oitassist.participation.dao.dto.response.InvitationListItemResponse;
69
import com.itasocialacademy.oitassist.participation.dao.dto.response.ProcessInvitationResponse;
710
import com.itasocialacademy.oitassist.participation.service.interfaces.InvitationService;
811
import io.swagger.v3.oas.annotations.Operation;
@@ -13,6 +16,9 @@
1316
import io.swagger.v3.oas.annotations.tags.Tag;
1417
import jakarta.validation.Valid;
1518
import lombok.RequiredArgsConstructor;
19+
import org.springdoc.core.annotations.ParameterObject;
20+
import org.springframework.data.domain.Pageable;
21+
import org.springframework.data.web.PageableDefault;
1622
import org.springframework.http.HttpStatus;
1723
import org.springframework.http.ResponseEntity;
1824
import org.springframework.security.access.prepost.PreAuthorize;
@@ -21,7 +27,7 @@
2127

2228
@RestController
2329
@RequiredArgsConstructor
24-
@RequestMapping("/api/v1/competitions/invitations")
30+
@RequestMapping("/api/v1")
2531
@Tag(name = "Invitation Manager v1", description = "Operations related to competition invitations")
2632
public class InvitationController {
2733
private final InvitationService invitationService;
@@ -54,11 +60,15 @@ public class InvitationController {
5460
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
5561
})
5662
@PreAuthorize("hasRole('ORG')")
57-
@PostMapping
63+
@PostMapping("/competitions/{competitionId}/stages/{stageId}/invitations")
5864
public ResponseEntity<CreateInvitationResponse> invite(
59-
@Valid @RequestBody CreateInvitationRequest createInvitationRequest) {
65+
@PathVariable Long competitionId,
66+
@PathVariable Long stageId,
67+
@Valid @RequestBody CreateInvitationRequest request) {
68+
request.setCompetitionId(competitionId);
69+
request.setStageId(stageId);
6070
return ResponseEntity.status(HttpStatus.CREATED)
61-
.body((CreateInvitationResponse) invitationService.sendEnrollmentRequest(createInvitationRequest));
71+
.body((CreateInvitationResponse) invitationService.sendEnrollmentRequest(request));
6272
}
6373

6474
@Operation(
@@ -77,7 +87,7 @@ public ResponseEntity<CreateInvitationResponse> invite(
7787
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
7888
})
7989
@PreAuthorize("hasRole('USER')")
80-
@PostMapping("/accept/{id}")
90+
@PostMapping("/enrollment/invitations/{id}/accept")
8191
public ResponseEntity<ProcessInvitationResponse> acceptRequest(@PathVariable Long id) {
8292
return ResponseEntity.status(HttpStatus.CREATED)
8393
.body((ProcessInvitationResponse) invitationService.acceptRequest(id));
@@ -98,7 +108,7 @@ public ResponseEntity<ProcessInvitationResponse> acceptRequest(@PathVariable Lon
98108
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
99109
})
100110
@PreAuthorize("hasRole('USER')")
101-
@PatchMapping("/reject/{id}")
111+
@PatchMapping("/enrollment/invitations/{id}/reject")
102112
public ResponseEntity<ProcessInvitationResponse> rejectRequest(
103113
@PathVariable Long id,
104114
@RequestBody RejectEnrollmentRequest rejectEnrollmentRequest) {
@@ -124,9 +134,42 @@ public ResponseEntity<ProcessInvitationResponse> rejectRequest(
124134
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
125135
})
126136
@PreAuthorize("hasRole('ORG')")
127-
@PatchMapping("/cancel/{id}")
137+
@PatchMapping("/enrollment/invitations/{id}/cancel")
128138
public ResponseEntity<ProcessInvitationResponse> cancelRequest(@PathVariable Long id) {
129139
return ResponseEntity.status(HttpStatus.OK)
130140
.body((ProcessInvitationResponse) invitationService.cancelRequest(id));
131141
}
142+
143+
@Operation(
144+
summary = "Get users' pending invitations",
145+
description = "Retrieves a paginated list of invitations "
146+
+ "for a specific competition stage.")
147+
@ApiResponses(value = {
148+
@ApiResponse(responseCode = "200", description = "Invitations retrieved successfully",
149+
content = @Content(mediaType = "application/json",
150+
schema = @Schema(implementation = PageResponse.class))),
151+
@ApiResponse(responseCode = "400", description = """
152+
The competition and stage info error. The reason: \s
153+
specified stage ID does not belong to the competition ID.""",
154+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
155+
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
156+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
157+
@ApiResponse(responseCode = "404", description = """
158+
Resource missing. Possible reasons:\s
159+
- The requested competition does not exist.\s
160+
- The requested stage does not exist.""",
161+
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
162+
})
163+
@PreAuthorize("hasRole('ORG')")
164+
@GetMapping("/competitions/{competitionId}/stages/{stageId}/invitations")
165+
public ResponseEntity<PageResponse<InvitationListItemResponse>> getRequests(
166+
@PathVariable Long competitionId,
167+
@PathVariable Long stageId,
168+
@RequestParam(required = false) String search,
169+
@ParameterObject @PageableDefault(size = 20, sort = "issuedAt") Pageable pageable) {
170+
EnrollmentRequestsFilter request = EnrollmentRequestsFilter.builder()
171+
.competitionId(competitionId).stageId(stageId).build();
172+
return ResponseEntity.status(HttpStatus.OK)
173+
.body(PageResponse.from(invitationService.getEnrollmentRequests(request, search, pageable)));
174+
}
132175
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.itasocialacademy.oitassist.participation.dao.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotNull;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
import lombok.Setter;
9+
import lombok.experimental.SuperBuilder;
10+
11+
@SuperBuilder
12+
@Getter
13+
@Setter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public abstract class AbstractEnrollmentFilter {
17+
@Schema(
18+
description = "Unique identifier of the competition",
19+
example = "1",
20+
requiredMode = Schema.RequiredMode.REQUIRED)
21+
@NotNull
22+
private Long competitionId;
23+
@Schema(
24+
description = "Unique identifier of the stage",
25+
example = "2",
26+
requiredMode = Schema.RequiredMode.REQUIRED)
27+
@NotNull
28+
private Long stageId;
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.itasocialacademy.oitassist.participation.dao.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.AllArgsConstructor;
5+
import lombok.EqualsAndHashCode;
6+
import lombok.Getter;
7+
import lombok.Setter;
8+
import lombok.experimental.SuperBuilder;
9+
10+
@Getter
11+
@Setter
12+
@AllArgsConstructor
13+
@EqualsAndHashCode(callSuper = true)
14+
@SuperBuilder
15+
@Schema(description = "DTO for getting the enrollment requests list.")
16+
public class EnrollmentRequestsFilter extends AbstractEnrollmentFilter {
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.itasocialacademy.oitassist.participation.dao.dto.response;
2+
3+
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import java.time.Instant;
6+
7+
@Schema(description = "DTO representing the each application request's details in the list.")
8+
public record ApplicationListItemResponse(
9+
@Schema(description = "Unique identifier of the request", example = "1") Long applicationId,
10+
@Schema(description = "Request creation date", example = "2026-06-07T09:50:30Z") Instant issuedAt,
11+
@Schema(description = "Current request status", example = "PENDING") RequestStatus status,
12+
UserSummary user) {
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.itasocialacademy.oitassist.participation.dao.dto.response;
2+
3+
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import java.time.Instant;
6+
7+
@Schema(description = "DTO representing the each invitation's details in the list.")
8+
public record InvitationListItemResponse(
9+
@Schema(description = "Unique identifier of the request", example = "1") Long invitationId,
10+
@Schema(description = "Request creation date", example = "2026-06-07T09:50:30Z") Instant issuedAt,
11+
@Schema(description = "Current request status", example = "PENDING") RequestStatus status,
12+
UserSummary user) {
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.itasocialacademy.oitassist.participation.dao.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "DTO representing the each user's details of each enrollment request in the list.")
6+
public record UserSummary(
7+
@Schema(description = "User's first name", example = "Ivan") String firstName,
8+
@Schema(description = "User's surname", example = "Petrenko") String surname,
9+
@Schema(description = "User's email address", example = "ivapetr@gmail.com") String email) {
10+
}

src/main/java/com/itasocialacademy/oitassist/participation/dao/repository/ApplicationRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
44
import com.itasocialacademy.oitassist.participation.dao.model.Application;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
67
import org.springframework.stereotype.Repository;
78

89
@Repository
9-
public interface ApplicationRepository extends JpaRepository<Application, Long> {
10+
public interface ApplicationRepository extends JpaRepository<Application, Long>, JpaSpecificationExecutor<Application> {
1011
boolean existsByIssuedByAndCompetitionIdAndStageIdAndStatus(
1112
Long studentId,
1213
Long competitionId,

src/main/java/com/itasocialacademy/oitassist/participation/dao/repository/InvitationRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
44
import com.itasocialacademy.oitassist.participation.dao.model.Invitation;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
67
import org.springframework.stereotype.Repository;
78
import java.util.List;
89

910
@Repository
10-
public interface InvitationRepository extends JpaRepository<Invitation, Long> {
11+
public interface InvitationRepository extends JpaRepository<Invitation, Long>, JpaSpecificationExecutor<Invitation> {
1112
List<Invitation> findByStudentIdInAndCompetitionIdAndStageIdAndStatus(
1213
List<Long> studentId,
1314
Long competitionId,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.itasocialacademy.oitassist.participation.dao.specification;
2+
3+
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
4+
import com.itasocialacademy.oitassist.participation.dao.model.Application;
5+
import lombok.AccessLevel;
6+
import lombok.NoArgsConstructor;
7+
import org.springframework.data.jpa.domain.Specification;
8+
import java.util.List;
9+
10+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
11+
public class ApplicationSpecification {
12+
public static Specification<Application> hasCompetitionAndStage(Long competitionId, Long stageId) {
13+
return (root, query, cb) -> cb.and(
14+
cb.equal(root.get("competitionId"), competitionId),
15+
cb.equal(root.get("stageId"), stageId));
16+
}
17+
18+
public static Specification<Application> userIdIn(List<Long> userIds) {
19+
return (root, query, cb) -> userIds == null
20+
? cb.conjunction()
21+
: root.get("issuedBy").in(userIds);
22+
}
23+
24+
public static Specification<Application> hasStatus(RequestStatus status) {
25+
return (root, query, cb) -> cb.and(
26+
cb.equal(root.get("status"), status));
27+
}
28+
}

0 commit comments

Comments
 (0)