Skip to content

Commit

Permalink
feat: ✨ 사용자 정의 카테고리 삭제 API (#123)
Browse files Browse the repository at this point in the history
* feat: 커스텀 카테고리 삭제 api 구현

* test: 통합 테스트 작성

* docs: swagger 작성

* fix: 사용하지않는 securityuserdetails 제거

* fix: conflict 해결시 발생한 syntax error fix
  • Loading branch information
asn6878 authored Jul 9, 2024
1 parent 74e0081 commit e80cf8b
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;


@Tag(name = "지출 카테고리 API")
public interface SpendingCategoryApi {
@Operation(summary = "지출 내역 카테고리 등록", method = "POST", description = "사용자 커스텀 지출 카테고리를 생성합니다.")
Expand All @@ -42,6 +43,20 @@ public interface SpendingCategoryApi {
@ApiResponse(responseCode = "200", description = "지출 카테고리 조회 성공", content = @Content(mediaType = "application/json", schemaProperties = @SchemaProperty(name = "spendingCategories", array = @ArraySchema(schema = @Schema(implementation = SpendingCategoryDto.Res.class)))))
ResponseEntity<?> getSpendingCategories(@AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "사용자 정의 카테고리 삭제", method = "DELETE", description = "사용자가 생성한 지출 카테고리를 삭제합니다.")
@Parameter(name = "categoryId", description = "카테고리 ID", example = "1", required = true, in = ParameterIn.PATH)
@ApiResponse(responseCode = "403", description = "지출 카테고리에 대한 권한이 없습니다.", content = @Content(examples = {
@ExampleObject(name = "지출 카테고리 권한 오류", description = "지출 카테고리에 대한 권한이 없습니다.",
value = """
{
"code": "4030",
"message": "ACCESS_TO_THE_REQUESTED_RESOURCE_IS_FORBIDDEN"
}
"""
)
}))
ResponseEntity<?> deleteSpendingCategory(@PathVariable Long categoryId);

@Operation(summary = "지출 카테고리에 등록된 지출 내역 총 개수 조회", method = "GET")
@Parameters({
@Parameter(name = "categoryId", description = "type이 default면 아이콘 코드(1~11), custom이면 카테고리 pk", required = true, in = ParameterIn.PATH),
Expand Down Expand Up @@ -114,3 +129,5 @@ ResponseEntity<?> getSpendingsByCategory(
@ApiResponse(responseCode = "200", description = "지출 카테고리 등록 성공", content = @Content(mediaType = "application/json", schemaProperties = @SchemaProperty(name = "spendingCategory", schema = @Schema(implementation = SpendingCategoryDto.Res.class))))
ResponseEntity<?> patchSpendingCategory(@PathVariable Long categoryId, @Validated SpendingCategoryDto.CreateParamReq param);
}


Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ public ResponseEntity<?> getSpendingCategories(@AuthenticationPrincipal Security
}

@Override
@DeleteMapping("/{categoryId}")
@PreAuthorize("isAuthenticated() and @spendingCategoryManager.hasPermission(principal.userId, #categoryId)")
public ResponseEntity<?> deleteSpendingCategory(@PathVariable Long categoryId) {
spendingCategoryUseCase.deleteSpendingCategory(categoryId);

return ResponseEntity.ok(SuccessResponse.noContent());
}

@GetMapping("/{categoryId}/spendings/count")
@PreAuthorize("isAuthenticated() and @spendingCategoryManager.hasPermission(#user.getUserId(), #categoryId, #type)")
public ResponseEntity<?> getSpendingTotalCountByCategory(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.domain.domains.spending.service.SpendingCustomCategoryService;
import kr.co.pennyway.domain.domains.spending.service.SpendingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class SpendingCategoryDeleteService {
private final SpendingCustomCategoryService spendingCustomCategoryService;
private final SpendingService spendingService;

@Transactional
public void execute(Long categoryId) {
spendingService.deleteSpendingsByCategoryIdInQuery(categoryId);
spendingCustomCategoryService.deleteSpendingCustomCategory(categoryId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import kr.co.pennyway.api.apis.ledger.dto.SpendingSearchRes;
import kr.co.pennyway.api.apis.ledger.mapper.SpendingCategoryMapper;
import kr.co.pennyway.api.apis.ledger.mapper.SpendingMapper;
import kr.co.pennyway.api.apis.ledger.service.SpendingCategoryDeleteService;
import kr.co.pennyway.api.apis.ledger.service.SpendingCategorySaveService;
import kr.co.pennyway.api.apis.ledger.service.SpendingCategorySearchService;
import kr.co.pennyway.api.apis.ledger.service.SpendingSearchService;
Expand All @@ -26,6 +27,7 @@
public class SpendingCategoryUseCase {
private final SpendingCategorySaveService spendingCategorySaveService;
private final SpendingCategorySearchService spendingCategorySearchService;
private final SpendingCategoryDeleteService spendingCategoryDeleteService;

private final SpendingSearchService spendingSearchService;

Expand All @@ -42,7 +44,12 @@ public List<SpendingCategoryDto.Res> getSpendingCategories(Long userId) {

return SpendingCategoryMapper.toResponses(categories);
}


@Transactional
public void deleteSpendingCategory(Long categoryId) {
spendingCategoryDeleteService.execute(categoryId);
}

@Transactional(readOnly = true)
public int getSpendingTotalCountByCategory(Long userId, Long categoryId, SpendingCategoryType type) {
return spendingSearchService.readSpendingTotalCountByCategoryId(userId, categoryId, type);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package kr.co.pennyway.api.apis.ledger.integration;

import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
import kr.co.pennyway.api.config.ExternalApiDBTestConfig;
import kr.co.pennyway.api.config.ExternalApiIntegrationTest;
import kr.co.pennyway.api.config.fixture.SpendingCustomCategoryFixture;
import kr.co.pennyway.api.config.fixture.SpendingFixture;
import kr.co.pennyway.api.config.fixture.UserFixture;
import kr.co.pennyway.domain.domains.spending.domain.Spending;
import kr.co.pennyway.domain.domains.spending.domain.SpendingCustomCategory;
import kr.co.pennyway.domain.domains.spending.service.SpendingCustomCategoryService;
import kr.co.pennyway.domain.domains.spending.service.SpendingService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Slf4j
@ExternalApiIntegrationTest
@AutoConfigureMockMvc
public class SpendingCategoryIntegrationTest extends ExternalApiDBTestConfig {

@Autowired
private MockMvc mockMvc;
@Autowired
private SpendingService spendingService;
@Autowired
private UserService userService;
@Autowired
private SpendingCustomCategoryService spendingCustomCategoryService;

@Test
@DisplayName("사용자 정의 지출 카테고리를 삭제하고, 삭제된 카테고리를 가지는 지출 내역 또한 삭제된다.")
@Transactional
void deleteSpendingCustomCategory() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
SpendingCustomCategory spendingCustomCategory = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategoryFixture.GENERAL_SPENDING_CUSTOM_CATEGORY.toCustomSpendingCategory(user));
Spending spending = spendingService.createSpending(SpendingFixture.CUSTOM_CATEGORY_SPENDING.toCustomCategorySpending(user, spendingCustomCategory));

// when
ResultActions resultActions = performDeleteSpendingCategory(spendingCustomCategory.getId(), user);

// then
resultActions
.andDo(print())
.andExpect(status().isOk());

Assertions.assertTrue(spendingCustomCategoryService.readSpendingCustomCategory(spendingCustomCategory.getId()).isEmpty());
Assertions.assertTrue(spendingService.readSpending(spending.getId()).isEmpty());
}

private ResultActions performDeleteSpendingCategory(Long categoryId, User requestUser) throws Exception {
UserDetails userDetails = SecurityUserDetails.from(requestUser);

return mockMvc.perform(MockMvcRequestBuilders.delete("/v2/spending-categories/{categoryId}", categoryId)
.with(user(userDetails)));
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public interface SpendingRepository extends ExtendedRepository<Spending, Long>,
@Transactional(readOnly = true)
boolean existsByIdAndUser_Id(Long id, Long userId);

@Transactional
@Modifying(clearAutomatically = true)
@Query("UPDATE Spending s SET s.deletedAt = NOW() WHERE s.spendingCustomCategory.id = :categoryId AND s.deletedAt IS NULL")
void deleteAllByCategoryIdAndDeletedAtNullInQuery(Long categoryId);

@Transactional(readOnly = true)
int countByUser_IdAndSpendingCustomCategory_Id(Long userId, Long categoryId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public List<SpendingCustomCategory> readSpendingCustomCategories(Long userId) {
public boolean isExistsSpendingCustomCategory(Long userId, Long categoryId) {
return spendingCustomCategoryRepository.existsByIdAndUser_Id(categoryId, userId);
}

@Transactional
public void deleteSpendingCustomCategory(Long categoryId) {
spendingCustomCategoryRepository.deleteById(categoryId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public List<TotalSpendingAmount> readTotalSpendingsAmountByUserId(Long userId) {
return spendingRepository.selectList(predicate, TotalSpendingAmount.class, bindings, queryHandler, sort);
}

@Transactional
public void deleteSpendingsByCategoryIdInQuery(Long categoryId) {
spendingRepository.deleteAllByCategoryIdAndDeletedAtNullInQuery(categoryId);
}

@Transactional(readOnly = true)
public boolean isExistsSpending(Long userId, Long spendingId) {
return spendingRepository.existsByIdAndUser_Id(spendingId, userId);
Expand Down

0 comments on commit e80cf8b

Please sign in to comment.