diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 556c45ff4..199e02fd6 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -32,6 +32,7 @@ public enum ExceptionMessage { RECRUITMENT_NOT_OPEN("현재 모집 진행 중이 아닙니다."), DUPLICATE_APPLICATION("이미 신청한 봉사 모집 공고입니다."), UNAUTHORIZED_VOLUNTEER_APPLY("해당 지원에 권한이 없습니다."), + RECRUIT_BOARD_ALREADY_COMPLETED("이미 종료된 봉사 활동입니다."), ; private final String message; diff --git a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java index d93b8f337..86689a036 100644 --- a/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java +++ b/src/main/java/com/somemore/recruitboard/domain/RecruitBoard.java @@ -1,5 +1,6 @@ package com.somemore.recruitboard.domain; +import static com.somemore.recruitboard.domain.RecruitStatus.COMPLETED; import static com.somemore.recruitboard.domain.RecruitStatus.RECRUITING; import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.GenerationType.IDENTITY; @@ -106,6 +107,10 @@ private void updateRecruitmentInfo(RecruitBoardUpdateRequestDto dto) { ); } + public boolean isCompleted() { + return this.recruitStatus == COMPLETED; + } + private void validateStatusChange(RecruitStatus newStatus) { if (newStatus.isChangeable()) { return; diff --git a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java index f0a4b5554..0c9f8e93d 100644 --- a/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java +++ b/src/main/java/com/somemore/recruitboard/usecase/query/RecruitBoardQueryUseCase.java @@ -7,14 +7,14 @@ import com.somemore.recruitboard.dto.response.RecruitBoardResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithCenterResponseDto; import com.somemore.recruitboard.dto.response.RecruitBoardWithLocationResponseDto; -import org.springframework.data.domain.Page; - import java.util.List; import java.util.UUID; +import org.springframework.data.domain.Page; public interface RecruitBoardQueryUseCase { RecruitBoard getById(Long id); + RecruitBoardResponseDto getRecruitBoardById(Long id); RecruitBoardWithLocationResponseDto getWithLocationById(Long id); @@ -25,7 +25,7 @@ Page getRecruitBoardsNearby( RecruitBoardNearByCondition condition); Page getRecruitBoardsByCenterId(UUID centerId, - RecruitBoardSearchCondition condition); + RecruitBoardSearchCondition condition); List getNotCompletedIdsByCenterIds(UUID centerId); diff --git a/src/main/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiController.java b/src/main/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiController.java new file mode 100644 index 000000000..77826ffbf --- /dev/null +++ b/src/main/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiController.java @@ -0,0 +1,50 @@ +package com.somemore.volunteerapply.controller; + +import com.somemore.auth.annotation.CurrentUser; +import com.somemore.global.common.response.ApiResponse; +import com.somemore.volunteerapply.usecase.ApproveVolunteerApplyUseCase; +import com.somemore.volunteerapply.usecase.RejectVolunteerApplyUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Center Volunteer Apply Command API", description = "봉사 활동 지원 승인, 거절, 정산 API") +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class CenterVolunteerApplyCommandApiController { + + private final ApproveVolunteerApplyUseCase approveVolunteerApplyUseCase; + private final RejectVolunteerApplyUseCase rejectVolunteerApplyUseCase; + + @Secured("ROLE_CENTER") + @Operation(summary = "봉사 활동 지원 승인", description = "봉사 활동 지원을 승인합니다.") + @PatchMapping("/volunteer-apply/{id}/approve") + public ApiResponse approve( + @CurrentUser UUID centerId, + @PathVariable Long id + ) { + + approveVolunteerApplyUseCase.approve(id, centerId); + return ApiResponse.ok("봉사 활동 지원 승인 성공"); + } + + @Secured("ROLE_CENTER") + @Operation(summary = "봉사 활동 지원 거절", description = "봉사 활동 지원을 거절합니다.") + @PatchMapping("/volunteer-apply/{id}/reject") + public ApiResponse reject( + @CurrentUser UUID centerId, + @PathVariable Long id + ) { + + rejectVolunteerApplyUseCase.reject(id, centerId); + return ApiResponse.ok("봉사 활동 지원 거절 성공"); + } + +} diff --git a/src/main/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyService.java b/src/main/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyService.java new file mode 100644 index 000000000..8e2f13338 --- /dev/null +++ b/src/main/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyService.java @@ -0,0 +1,58 @@ +package com.somemore.volunteerapply.service; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER_APPLY; +import static com.somemore.global.exception.ExceptionMessage.RECRUIT_BOARD_ALREADY_COMPLETED; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import com.somemore.volunteerapply.usecase.ApproveVolunteerApplyUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class ApproveVolunteerApplyService implements ApproveVolunteerApplyUseCase { + + private final VolunteerApplyRepository volunteerApplyRepository; + + private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; + + @Override + public void approve(Long id, UUID centerId) { + VolunteerApply apply = getVolunteerApply(id); + RecruitBoard recruitBoard = recruitBoardQueryUseCase.getById(apply.getRecruitBoardId()); + + validateWriter(recruitBoard, centerId); + validateBoardStatus(recruitBoard); + + apply.changeStatus(APPROVED); + volunteerApplyRepository.save(apply); + } + + private VolunteerApply getVolunteerApply(Long id) { + return volunteerApplyRepository.findById(id).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_VOLUNTEER_APPLY) + ); + } + + private void validateWriter(RecruitBoard recruitBoard, UUID centerId) { + if (recruitBoard.isWriter(centerId)) { + return; + } + throw new BadRequestException(UNAUTHORIZED_RECRUIT_BOARD); + } + + private void validateBoardStatus(RecruitBoard recruitBoard) { + if (recruitBoard.isCompleted()) { + throw new BadRequestException(RECRUIT_BOARD_ALREADY_COMPLETED); + } + } +} diff --git a/src/main/java/com/somemore/volunteerapply/service/RejectVolunteerApplyService.java b/src/main/java/com/somemore/volunteerapply/service/RejectVolunteerApplyService.java new file mode 100644 index 000000000..d8a0da337 --- /dev/null +++ b/src/main/java/com/somemore/volunteerapply/service/RejectVolunteerApplyService.java @@ -0,0 +1,58 @@ +package com.somemore.volunteerapply.service; + +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER_APPLY; +import static com.somemore.global.exception.ExceptionMessage.RECRUIT_BOARD_ALREADY_COMPLETED; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; +import static com.somemore.volunteerapply.domain.ApplyStatus.REJECTED; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.usecase.query.RecruitBoardQueryUseCase; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import com.somemore.volunteerapply.usecase.RejectVolunteerApplyUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class RejectVolunteerApplyService implements RejectVolunteerApplyUseCase { + + private final VolunteerApplyRepository volunteerApplyRepository; + private final RecruitBoardQueryUseCase recruitBoardQueryUseCase; + + @Override + public void reject(Long id, UUID centerId) { + VolunteerApply apply = getApply(id); + RecruitBoard recruitBoard = recruitBoardQueryUseCase.getById(apply.getRecruitBoardId()); + + validateWriter(recruitBoard, centerId); + validateBoardStatus(recruitBoard); + + apply.changeStatus(REJECTED); + volunteerApplyRepository.save(apply); + } + + private VolunteerApply getApply(Long id) { + return volunteerApplyRepository.findById(id).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_VOLUNTEER_APPLY) + ); + } + + private void validateWriter(RecruitBoard recruitBoard, UUID centerId) { + if (recruitBoard.isWriter(centerId)) { + return; + } + throw new BadRequestException(UNAUTHORIZED_RECRUIT_BOARD); + } + + private void validateBoardStatus(RecruitBoard recruitBoard) { + if (recruitBoard.isCompleted()) { + throw new BadRequestException(RECRUIT_BOARD_ALREADY_COMPLETED); + } + } + +} diff --git a/src/main/java/com/somemore/volunteerapply/usecase/ApproveVolunteerApplyUseCase.java b/src/main/java/com/somemore/volunteerapply/usecase/ApproveVolunteerApplyUseCase.java new file mode 100644 index 000000000..002a3699c --- /dev/null +++ b/src/main/java/com/somemore/volunteerapply/usecase/ApproveVolunteerApplyUseCase.java @@ -0,0 +1,10 @@ +package com.somemore.volunteerapply.usecase; + + +import java.util.UUID; + +public interface ApproveVolunteerApplyUseCase { + + void approve(Long id, UUID centerId); + +} diff --git a/src/main/java/com/somemore/volunteerapply/usecase/RejectVolunteerApplyUseCase.java b/src/main/java/com/somemore/volunteerapply/usecase/RejectVolunteerApplyUseCase.java new file mode 100644 index 000000000..a37697550 --- /dev/null +++ b/src/main/java/com/somemore/volunteerapply/usecase/RejectVolunteerApplyUseCase.java @@ -0,0 +1,9 @@ +package com.somemore.volunteerapply.usecase; + +import java.util.UUID; + +public interface RejectVolunteerApplyUseCase { + + void reject(Long id, UUID centerId); + +} diff --git a/src/test/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiControllerTest.java b/src/test/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiControllerTest.java new file mode 100644 index 000000000..2cc773ee0 --- /dev/null +++ b/src/test/java/com/somemore/volunteerapply/controller/CenterVolunteerApplyCommandApiControllerTest.java @@ -0,0 +1,68 @@ +package com.somemore.volunteerapply.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willDoNothing; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.somemore.ControllerTestSupport; +import com.somemore.WithMockCustomUser; +import com.somemore.volunteerapply.usecase.ApproveVolunteerApplyUseCase; +import com.somemore.volunteerapply.usecase.RejectVolunteerApplyUseCase; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +class CenterVolunteerApplyCommandApiControllerTest extends ControllerTestSupport { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ApproveVolunteerApplyUseCase approveVolunteerApplyUseCase; + + @MockBean + private RejectVolunteerApplyUseCase rejectVolunteerApplyUseCase; + + @Test + @DisplayName("봉사 활동 지원 승인 성공 테스트") + @WithMockCustomUser(role = "CENTER") + void approve() throws Exception { + // given + Long id = 1L; + + willDoNothing().given(approveVolunteerApplyUseCase) + .approve(any(), any(UUID.class)); + // when + mockMvc.perform(patch("/api/volunteer-apply/{id}/approve", id) + .header("Authorization", "Bearer access-token")) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value("")) + .andExpect(jsonPath("$.message").value("봉사 활동 지원 승인 성공")); + } + + @Test + @DisplayName("봉사 활동 지원 거절 성공 테스트") + @WithMockCustomUser(role = "CENTER") + void reject() throws Exception { + // given + Long id = 1L; + + willDoNothing().given(rejectVolunteerApplyUseCase) + .reject(any(), any(UUID.class)); + // when + mockMvc.perform(patch("/api/volunteer-apply/{id}/reject", id) + .header("Authorization", "Bearer access-token")) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").value("")) + .andExpect(jsonPath("$.message").value("봉사 활동 지원 거절 성공")); + } +} diff --git a/src/test/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyServiceTest.java b/src/test/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyServiceTest.java new file mode 100644 index 000000000..395d26f43 --- /dev/null +++ b/src/test/java/com/somemore/volunteerapply/service/ApproveVolunteerApplyServiceTest.java @@ -0,0 +1,106 @@ +package com.somemore.volunteerapply.service; + +import static com.somemore.common.fixture.RecruitBoardFixture.createCompletedRecruitBoard; +import static com.somemore.common.fixture.RecruitBoardFixture.createRecruitBoard; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; +import static com.somemore.recruitboard.domain.VolunteerCategory.OTHER; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; +import static com.somemore.volunteerapply.domain.ApplyStatus.WAITING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.ExceptionMessage; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ApproveVolunteerApplyServiceTest extends IntegrationTestSupport { + + @Autowired + private ApproveVolunteerApplyService approveVolunteerApplyService; + + @Autowired + private VolunteerApplyRepository volunteerApplyRepository; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + @DisplayName("봉사 지원을 승인할 수 있다.") + @Test + void approve() { + // given + UUID centerId = UUID.randomUUID(); + + RecruitBoard board = createRecruitBoard(centerId); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + // when + approveVolunteerApplyService.approve(apply.getId(), centerId); + + // then + VolunteerApply approve = volunteerApplyRepository.findById(apply.getId()).orElseThrow(); + + assertThat(approve.getStatus()).isEqualTo(APPROVED); + } + + @DisplayName("자신이 작성한 모집글이 아닌 신청에 대해 승인할 경우 에러가 발생한다.") + @Test + void approveWithWrongCenter() { + // given + RecruitBoard board = createRecruitBoard(UUID.randomUUID()); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + Long id = apply.getId(); + UUID wrongCenterId = UUID.randomUUID(); + + // when + // then + assertThatThrownBy( + () -> approveVolunteerApplyService.approve(id, wrongCenterId) + ).isInstanceOf(BadRequestException.class) + .hasMessage(UNAUTHORIZED_RECRUIT_BOARD.getMessage()); + } + + @DisplayName("이미 완료된 봉사 모집에 대해 지원을 승인할 경우 에러가 발생 한다.") + @Test + void approveWithAlreadyCompletedRecruit() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard board = createCompletedRecruitBoard(centerId, OTHER); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + Long id = apply.getId(); + // when + // then + assertThatThrownBy( + () -> approveVolunteerApplyService.approve(id, centerId) + ).isInstanceOf(BadRequestException.class) + .hasMessage(ExceptionMessage.RECRUIT_BOARD_ALREADY_COMPLETED.getMessage()); + } + + private VolunteerApply createApply(Long recruitBoardId) { + return VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(recruitBoardId) + .status(WAITING) + .attended(false) + .build(); + } + +} diff --git a/src/test/java/com/somemore/volunteerapply/service/RejectVolunteerApplyServiceTest.java b/src/test/java/com/somemore/volunteerapply/service/RejectVolunteerApplyServiceTest.java new file mode 100644 index 000000000..7ba23e55f --- /dev/null +++ b/src/test/java/com/somemore/volunteerapply/service/RejectVolunteerApplyServiceTest.java @@ -0,0 +1,105 @@ +package com.somemore.volunteerapply.service; + +import static com.somemore.common.fixture.RecruitBoardFixture.createCompletedRecruitBoard; +import static com.somemore.common.fixture.RecruitBoardFixture.createRecruitBoard; +import static com.somemore.global.exception.ExceptionMessage.UNAUTHORIZED_RECRUIT_BOARD; +import static com.somemore.recruitboard.domain.VolunteerCategory.OTHER; +import static com.somemore.volunteerapply.domain.ApplyStatus.REJECTED; +import static com.somemore.volunteerapply.domain.ApplyStatus.WAITING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.ExceptionMessage; +import com.somemore.recruitboard.domain.RecruitBoard; +import com.somemore.recruitboard.repository.RecruitBoardRepository; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class RejectVolunteerApplyServiceTest extends IntegrationTestSupport { + + @Autowired + private RejectVolunteerApplyService rejectVolunteerApplyService; + + @Autowired + private VolunteerApplyRepository volunteerApplyRepository; + + @Autowired + private RecruitBoardRepository recruitBoardRepository; + + @DisplayName("봉사 지원을 거절할 수 있다.") + @Test + void reject() { + // given + UUID centerId = UUID.randomUUID(); + + RecruitBoard board = createRecruitBoard(centerId); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + // when + rejectVolunteerApplyService.reject(apply.getId(), centerId); + + // then + VolunteerApply approve = volunteerApplyRepository.findById(apply.getId()).orElseThrow(); + + assertThat(approve.getStatus()).isEqualTo(REJECTED); + } + + @DisplayName("자신이 작성한 모집글이 아닌 신청에 대해 거절할 경우 에러가 발생한다.") + @Test + void rejectWithWrongCenter() { + // given + RecruitBoard board = createRecruitBoard(UUID.randomUUID()); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + Long id = apply.getId(); + UUID wrongCenterId = UUID.randomUUID(); + + // when + // then + assertThatThrownBy( + () -> rejectVolunteerApplyService.reject(id, wrongCenterId) + ).isInstanceOf(BadRequestException.class) + .hasMessage(UNAUTHORIZED_RECRUIT_BOARD.getMessage()); + } + + @DisplayName("이미 완료된 봉사 모집에 대해 지원을 거절할 경우 에러가 발생 한다.") + @Test + void rejectWithAlreadyCompletedRecruit() { + // given + UUID centerId = UUID.randomUUID(); + RecruitBoard board = createCompletedRecruitBoard(centerId, OTHER); + recruitBoardRepository.save(board); + + VolunteerApply apply = createApply(board.getId()); + volunteerApplyRepository.save(apply); + + Long id = apply.getId(); + // when + // then + assertThatThrownBy( + () -> rejectVolunteerApplyService.reject(id, centerId) + ).isInstanceOf(BadRequestException.class) + .hasMessage(ExceptionMessage.RECRUIT_BOARD_ALREADY_COMPLETED.getMessage()); + } + + private VolunteerApply createApply(Long recruitBoardId) { + return VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(recruitBoardId) + .status(WAITING) + .attended(false) + .build(); + } +}