Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.itasocialacademy.oitassist.participation.controller;

import com.itasocialacademy.oitassist.core.dao.dto.response.PageResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.request.CreateApplicationRequest;
import com.itasocialacademy.oitassist.participation.dao.dto.request.EnrollmentRequestsFilter;
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectEnrollmentRequest;
import com.itasocialacademy.oitassist.participation.dao.dto.response.ApplicationListItemResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.response.CreateApplicationResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.response.ProcessApplicationResponse;
import com.itasocialacademy.oitassist.participation.service.interfaces.ApplicationService;
Expand All @@ -11,8 +14,10 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -21,7 +26,7 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/competitions/applications")
@RequestMapping("/api/v1")
@Tag(name = "Application Manager v1", description = "Operations related to competition applications")
public class ApplicationController {
private final ApplicationService applicationService;
Expand Down Expand Up @@ -51,11 +56,14 @@ public class ApplicationController {
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('USER')")
@PostMapping
@PostMapping("/competitions/{competitionId}/stages/{stageId}/applications")
public ResponseEntity<CreateApplicationResponse> apply(
@Valid @RequestBody CreateApplicationRequest createApplicationRequest) {
@PathVariable Long competitionId,
@PathVariable Long stageId) {
CreateApplicationRequest request = CreateApplicationRequest.builder()
.competitionId(competitionId).stageId(stageId).build();
return ResponseEntity.status(HttpStatus.CREATED)
.body((CreateApplicationResponse) applicationService.sendEnrollmentRequest(createApplicationRequest));
.body((CreateApplicationResponse) applicationService.sendEnrollmentRequest(request));
}

@Operation(
Expand All @@ -74,7 +82,7 @@ public ResponseEntity<CreateApplicationResponse> apply(
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@PostMapping("/accept/{id}")
@PostMapping("/enrollment/applications/{id}/accept")
public ResponseEntity<ProcessApplicationResponse> acceptRequest(@PathVariable Long id) {
return ResponseEntity.status(HttpStatus.CREATED)
.body((ProcessApplicationResponse) applicationService.acceptRequest(id));
Expand All @@ -95,7 +103,7 @@ public ResponseEntity<ProcessApplicationResponse> acceptRequest(@PathVariable Lo
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@PatchMapping("/reject/{id}")
@PatchMapping("/enrollment/applications/{id}/reject")
public ResponseEntity<ProcessApplicationResponse> rejectRequest(
@PathVariable Long id,
@RequestBody RejectEnrollmentRequest rejectEnrollmentRequest) {
Expand All @@ -111,7 +119,7 @@ public ResponseEntity<ProcessApplicationResponse> rejectRequest(
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ProcessApplicationResponse.class))),
@ApiResponse(responseCode = "400", description = """
User is unable to apply. Possible reasons:\s
User is unable to cancel the request. Possible reasons:\s
- The user is not the owner of the application.\s
- The application request's status is not PENDING.""",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
Expand All @@ -121,9 +129,42 @@ public ResponseEntity<ProcessApplicationResponse> rejectRequest(
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('USER')")
@PatchMapping("/cancel/{id}")
@PatchMapping("/enrollment/applications/{id}/cancel")
public ResponseEntity<ProcessApplicationResponse> cancelRequest(@PathVariable Long id) {
return ResponseEntity.status(HttpStatus.OK)
.body((ProcessApplicationResponse) applicationService.cancelRequest(id));
}

@Operation(
summary = "Get users' application requests",
description = "Retrieves a paginated list of application requests "
+ "for a specific competition stage.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Applications retrieved successfully",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ApplicationListItemResponse.class))),
@ApiResponse(responseCode = "400", description = """
The competition and stage info error. The reason: \s
specified stage ID does not belong to the competition ID.""",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = """
Resource missing. Possible reasons:\s
- The requested competition does not exist.\s
- The requested stage does not exist.""",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@GetMapping("/competitions/{competitionId}/stages/{stageId}/applications")
public ResponseEntity<PageResponse<ApplicationListItemResponse>> getRequests(
@PathVariable Long competitionId,
@PathVariable Long stageId,
@RequestParam(required = false) String search,
@ParameterObject @PageableDefault(size = 20, sort = "issuedAt") Pageable pageable) {
EnrollmentRequestsFilter request = EnrollmentRequestsFilter.builder()
.competitionId(competitionId).stageId(stageId).build();
return ResponseEntity.status(HttpStatus.OK)
.body(PageResponse.from(applicationService.getEnrollmentRequests(request, search, pageable)));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.itasocialacademy.oitassist.participation.controller;

import com.itasocialacademy.oitassist.core.dao.dto.response.PageResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.request.CreateInvitationRequest;
import com.itasocialacademy.oitassist.participation.dao.dto.request.EnrollmentRequestsFilter;
import com.itasocialacademy.oitassist.participation.dao.dto.request.RejectEnrollmentRequest;
import com.itasocialacademy.oitassist.participation.dao.dto.response.CreateInvitationResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.response.InvitationListItemResponse;
import com.itasocialacademy.oitassist.participation.dao.dto.response.ProcessInvitationResponse;
import com.itasocialacademy.oitassist.participation.service.interfaces.InvitationService;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -13,6 +16,9 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -21,7 +27,7 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/competitions/invitations")
@RequestMapping("/api/v1")
@Tag(name = "Invitation Manager v1", description = "Operations related to competition invitations")
public class InvitationController {
private final InvitationService invitationService;
Expand Down Expand Up @@ -54,8 +60,10 @@ public class InvitationController {
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@PostMapping
@PostMapping("/competitions/{competitionId}/stages/{stageId}/invitations")
public ResponseEntity<CreateInvitationResponse> invite(
@PathVariable Long competitionId,
@PathVariable Long stageId,
@Valid @RequestBody CreateInvitationRequest createInvitationRequest) {
return ResponseEntity.status(HttpStatus.CREATED)
.body((CreateInvitationResponse) invitationService.sendEnrollmentRequest(createInvitationRequest));
Comment thread
serggalel marked this conversation as resolved.
Outdated
Expand All @@ -77,7 +85,7 @@ public ResponseEntity<CreateInvitationResponse> invite(
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('USER')")
@PostMapping("/accept/{id}")
@PostMapping("/enrollment/invitations/{id}/accept")
public ResponseEntity<ProcessInvitationResponse> acceptRequest(@PathVariable Long id) {
return ResponseEntity.status(HttpStatus.CREATED)
.body((ProcessInvitationResponse) invitationService.acceptRequest(id));
Expand All @@ -98,7 +106,7 @@ public ResponseEntity<ProcessInvitationResponse> acceptRequest(@PathVariable Lon
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('USER')")
@PatchMapping("/reject/{id}")
@PatchMapping("/enrollment/invitations/{id}/reject")
public ResponseEntity<ProcessInvitationResponse> rejectRequest(
@PathVariable Long id,
@RequestBody RejectEnrollmentRequest rejectEnrollmentRequest) {
Expand All @@ -124,9 +132,42 @@ public ResponseEntity<ProcessInvitationResponse> rejectRequest(
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@PatchMapping("/cancel/{id}")
@PatchMapping("/enrollment/invitations/{id}/cancel")
public ResponseEntity<ProcessInvitationResponse> cancelRequest(@PathVariable Long id) {
return ResponseEntity.status(HttpStatus.OK)
.body((ProcessInvitationResponse) invitationService.cancelRequest(id));
}

@Operation(
summary = "Get users' pending invitations",
description = "Retrieves a paginated list of invitations "
+ "for a specific competition stage.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Invitations retrieved successfully",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = InvitationListItemResponse.class))),
@ApiResponse(responseCode = "400", description = """
The competition and stage info error. The reason: \s
specified stage ID does not belong to the competition ID.""",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "403", description = "Access denied (requires ORG role)",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "404", description = """
Resource missing. Possible reasons:\s
- The requested competition does not exist.\s
- The requested stage does not exist.""",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class)))
})
@PreAuthorize("hasRole('ORG')")
@GetMapping("/competitions/{competitionId}/stages/{stageId}/invitations")
public ResponseEntity<PageResponse<InvitationListItemResponse>> getRequests(
@PathVariable Long competitionId,
@PathVariable Long stageId,
@RequestParam(required = false) String search,
@ParameterObject @PageableDefault(size = 20, sort = "issuedAt") Pageable pageable) {
EnrollmentRequestsFilter request = EnrollmentRequestsFilter.builder()
.competitionId(competitionId).stageId(stageId).build();
return ResponseEntity.status(HttpStatus.OK)
.body(PageResponse.from(invitationService.getEnrollmentRequests(request, search, pageable)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.itasocialacademy.oitassist.participation.dao.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

@SuperBuilder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public abstract class AbstractEnrollmentFilter {
@Schema(
description = "Unique identifier of the competition",
example = "1",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private Long competitionId;
@Schema(
description = "Unique identifier of the stage",
example = "2",
requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull
private Long stageId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.itasocialacademy.oitassist.participation.dao.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

@Getter
@Setter
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@Schema(description = "DTO for getting the enrollment requests list.")
public class EnrollmentRequestsFilter extends AbstractEnrollmentFilter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.itasocialacademy.oitassist.participation.dao.dto.response;

import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;

@Schema(description = "DTO representing the each application request's details in the list.")
public record ApplicationListItemResponse(
@Schema(description = "Unique identifier of the request", example = "1") Long applicationId,
@Schema(description = "Request creation date", example = "2026-06-07T09:50:30Z") Instant issuedAt,
@Schema(description = "Current request status", example = "PENDING") RequestStatus status,
UserSummary user) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.itasocialacademy.oitassist.participation.dao.dto.response;

import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.Instant;

@Schema(description = "DTO representing the each invitation's details in the list.")
public record InvitationListItemResponse(
@Schema(description = "Unique identifier of the request", example = "1") Long invitationId,
@Schema(description = "Request creation date", example = "2026-06-07T09:50:30Z") Instant issuedAt,
@Schema(description = "Current request status", example = "PENDING") RequestStatus status,
UserSummary user) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.itasocialacademy.oitassist.participation.dao.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "DTO representing the each user's details of each enrollment request in the list.")
public record UserSummary(
@Schema(description = "User's first name", example = "Ivan") String firstName,
@Schema(description = "User's surname", example = "Petrenko") String surname,
@Schema(description = "User's email address", example = "ivapetr@gmail.com") String email) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
import com.itasocialacademy.oitassist.participation.dao.model.Application;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface ApplicationRepository extends JpaRepository<Application, Long> {
public interface ApplicationRepository extends JpaRepository<Application, Long>, JpaSpecificationExecutor<Application> {
boolean existsByIssuedByAndCompetitionIdAndStageIdAndStatus(
Long studentId,
Long competitionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
import com.itasocialacademy.oitassist.participation.dao.model.Invitation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface InvitationRepository extends JpaRepository<Invitation, Long> {
public interface InvitationRepository extends JpaRepository<Invitation, Long>, JpaSpecificationExecutor<Invitation> {
List<Invitation> findByStudentIdInAndCompetitionIdAndStageIdAndStatus(
List<Long> studentId,
Long competitionId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.itasocialacademy.oitassist.participation.dao.specification;

import com.itasocialacademy.oitassist.participation.dao.enums.RequestStatus;
import com.itasocialacademy.oitassist.participation.dao.model.Application;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import java.util.List;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ApplicationSpecification {
public static Specification<Application> hasCompetitionAndStage(Long competitionId, Long stageId) {
return (root, query, cb) -> cb.and(
cb.equal(root.get("competitionId"), competitionId),
cb.equal(root.get("stageId"), stageId));
}

public static Specification<Application> userIdIn(List<Long> userIds) {
return (root, query, cb) -> userIds == null
? cb.conjunction()
: root.get("issuedBy").in(userIds);
}

public static Specification<Application> hasStatus(RequestStatus status) {
return (root, query, cb) -> cb.and(
cb.equal(root.get("status"), status));
}
}
Loading
Loading