Skip to content

Commit

Permalink
feat: ✨ 사용자 정의 카테고리 이동 API (#125)
Browse files Browse the repository at this point in the history
* feat: controller, usecase 작성

* feat: update service, domain service 작성

* docs: swagger 작성

* test: 지출 카테고리 수정 테스트 작성

* fix: controller 파라미터 수정

* fix: spel 표현식 제거

* test: 기본 카테고리 이동 테스트 작성

* docs: api 문서 이동

* refactor: controller 및 usecase를 spending 에서 spendingcategory로 이전

* feat: 기본 카테고리로부터의 이전 기능 추가

* feat: 권한검사 추가

* test: 각케이스별 테스트케이스 작성

* fix: 권한검사 로직 이동

* fix: spendingerrorcode 상수 제거

* fix: service 메서드 접두사 udpate로 수정

* fix: 접두사 udpate로 추가 수정

* fix: 불필요한 schema 제거

* fix: service 및 repository 메서드명 prefix customcategory로변경
  • Loading branch information
asn6878 authored Jul 31, 2024
1 parent 2f0a100 commit 5566c29
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,35 @@ 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);

@Operation(summary = "지출 내역 카테코리 이동", method = "PATCH", description = "카테고리에 존재하는 지출내역들을 다른 카테고리로 옮깁니다.")
@Parameters({
@Parameter(name = "fromId", description = "현재 선택된 카테고리 ID", required = true, in = ParameterIn.PATH),
@Parameter(name = "fromType", description = "현재 선택된 지출 카테고리 타입", required = true, in = ParameterIn.QUERY, examples = {
@ExampleObject(name = "기본", value = "default"), @ExampleObject(name = "사용자 정의", value = "custom")
}),
@Parameter(name = "toId", description = "이동 하고자 하는 카테고리 ID", required = true, in = ParameterIn.QUERY),
@Parameter(name = "toType", description = "이동 하고자 하는 지출 카테고리 타입", required = true, in = ParameterIn.QUERY, examples = {
@ExampleObject(name = "기본", value = "default"), @ExampleObject(name = "사용자 정의", value = "custom")
})
})
@ApiResponse(responseCode = "403", description = "지출 카테고리에 대한 권한이 없습니다.", content = @Content(examples = {
@ExampleObject(name = "지출 카테고리 권한 오류", description = "지출 카테고리에 대한 권한이 없습니다.",
value = """
{
"code": "4030",
"message": "ACCESS_TO_THE_REQUESTED_RESOURCE_IS_FORBIDDEN"
}
"""
)
}))
public ResponseEntity<?> migrateSpendingsByCategory(
@PathVariable Long fromId,
@RequestParam(value = "fromType") SpendingCategoryType fromType,
@RequestParam(value = "toId") Long toId,
@RequestParam(value = "toType") SpendingCategoryType toType,
@AuthenticationPrincipal SecurityUserDetails user
);
}


Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,22 @@ public ResponseEntity<?> patchSpendingCategory(@PathVariable Long categoryId, @V

return ResponseEntity.ok(SuccessResponse.from("spendingCategory", spendingCategoryUseCase.updateSpendingCategory(categoryId, param.name(), param.icon())));
}

@Override
@PatchMapping({"{fromId}/migration"})
@PreAuthorize("isAuthenticated() and @spendingCategoryManager.hasPermission(principal.userId, #fromId, #fromType) and @spendingCategoryManager.hasPermission(principal.userId, #toId, #toType)")
public ResponseEntity<?> migrateSpendingsByCategory(
@PathVariable Long fromId,
@RequestParam(value = "fromType") SpendingCategoryType fromType,
@RequestParam(value = "toId") Long toId,
@RequestParam(value = "toType") SpendingCategoryType toType,
@AuthenticationPrincipal SecurityUserDetails user
) {
Long userId = user.getUserId();
spendingCategoryUseCase.migrateSpendingsByCategory(fromId, fromType, toId, toType, userId);

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


}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.api.apis.ledger.dto.SpendingReq;
import kr.co.pennyway.api.common.query.SpendingCategoryType;
import kr.co.pennyway.api.common.security.authorization.SpendingCategoryManager;
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.exception.SpendingErrorCode;
import kr.co.pennyway.domain.domains.spending.exception.SpendingErrorException;
import kr.co.pennyway.domain.domains.spending.service.SpendingCustomCategoryService;
import kr.co.pennyway.domain.domains.spending.service.SpendingService;
import kr.co.pennyway.domain.domains.spending.type.SpendingCategory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -18,6 +21,7 @@
public class SpendingUpdateService {
private final SpendingService spendingService;
private final SpendingCustomCategoryService spendingCustomCategoryService;
private final SpendingCategoryManager spendingCategoryManager;

@Transactional
public Spending updateSpending(Long spendingId, SpendingReq request) {
Expand All @@ -31,4 +35,24 @@ public Spending updateSpending(Long spendingId, SpendingReq request) {

return spending;
}
}

@Transactional
public void migrateSpendings(Long fromId, SpendingCategoryType fromType, Long toId, SpendingCategoryType toType, Long userId) {
if (fromType.equals(SpendingCategoryType.DEFAULT)) {
SpendingCategory fromCategory = SpendingCategory.fromCode(fromId.toString());
if (toType.equals(SpendingCategoryType.CUSTOM)) {
spendingService.updateCategoryByCustomCategory(fromCategory, toId);
} else {
SpendingCategory spendingCategory = SpendingCategory.fromCode(toId.toString());
spendingService.updateCategoryByCategory(fromCategory, spendingCategory);
}
} else {
if (toType.equals(SpendingCategoryType.CUSTOM)) {
spendingService.updateCustomCategoryByCustomCategory(fromId, toId);
} else {
SpendingCategory spendingCategory = SpendingCategory.fromCode(toId.toString());
spendingService.updateCustomCategoryByCategory(fromId, spendingCategory);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +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;
import kr.co.pennyway.api.apis.ledger.service.*;
import kr.co.pennyway.api.common.query.SpendingCategoryType;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.domains.spending.domain.Spending;
Expand All @@ -30,6 +27,7 @@ public class SpendingCategoryUseCase {
private final SpendingCategoryDeleteService spendingCategoryDeleteService;

private final SpendingSearchService spendingSearchService;
private final SpendingUpdateService spendingUpdateService;

@Transactional
public SpendingCategoryDto.Res createSpendingCategory(Long userId, String categoryName, SpendingCategory icon) {
Expand Down Expand Up @@ -68,4 +66,9 @@ public SpendingCategoryDto.Res updateSpendingCategory(Long categoryId, String na

return SpendingCategoryMapper.toResponse(category);
}

@Transactional
public void migrateSpendingsByCategory(Long fromId, SpendingCategoryType fromType, Long toId, SpendingCategoryType toType, Long userId) {
spendingUpdateService.migrateSpendings(fromId, fromType, toId, toType, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ public void deleteSpending(Long spendingId) {
public void deleteSpendings(List<Long> spendingIds) {
spendingDeleteService.deleteSpendings(spendingIds);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
public enum SpendingCategoryType {
DEFAULT("default"),
CUSTOM("custom");

private final String type;

SpendingCategoryType(String type) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.co.pennyway.api.apis.ledger.integration;

import kr.co.pennyway.api.common.query.SpendingCategoryType;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
import kr.co.pennyway.api.config.ExternalApiDBTestConfig;
import kr.co.pennyway.api.config.ExternalApiIntegrationTest;
Expand All @@ -10,6 +11,7 @@
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.spending.type.SpendingCategory;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -71,4 +73,106 @@ private ResultActions performDeleteSpendingCategory(Long categoryId, User reques
}


@Test
@DisplayName("사용자는 커스텀 카테고리에서 기본 카테고리로 지출내역들을 옮길 수 있다.")
void migrateSpendingsCtoD() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
SpendingCustomCategory fromCategory = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategoryFixture.GENERAL_SPENDING_CUSTOM_CATEGORY.toCustomSpendingCategory(user));
Spending spending = spendingService.createSpending(SpendingFixture.CUSTOM_CATEGORY_SPENDING.toCustomCategorySpending(user, fromCategory));
Long spendingId = spending.getId();
SpendingCategory toCategory = SpendingCategory.TRANSPORTATION;

// when
ResultActions resultActions = performMigrateSpendingsByCategory(fromCategory.getId(), SpendingCategoryType.CUSTOM, 2L, SpendingCategoryType.DEFAULT, user);

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

Spending spendingAfterMigration = spendingService.readSpending(spendingId).orElseThrow();
Assertions.assertEquals(spendingAfterMigration.getCategory().icon(), toCategory);
Assertions.assertEquals(spendingAfterMigration.getSpendingCustomCategory(), null);
}

@Test
@DisplayName("사용자는 커스텀 카테고리에서 커스텀 카테고리로 지출내역들을 옮길 수 있다.")
void migrateSpendingsCtoC() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
SpendingCustomCategory fromCategory = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategoryFixture.GENERAL_SPENDING_CUSTOM_CATEGORY.toCustomSpendingCategory(user));
Spending spending = spendingService.createSpending(SpendingFixture.CUSTOM_CATEGORY_SPENDING.toCustomCategorySpending(user, fromCategory));

SpendingCustomCategory toCategory = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategoryFixture.GENERAL_SPENDING_CUSTOM_CATEGORY.toCustomSpendingCategory(user));
Long toCategoryId = toCategory.getId();
Long spendingId = spending.getId();

// when
ResultActions resultActions = performMigrateSpendingsByCategory(fromCategory.getId(), SpendingCategoryType.CUSTOM, toCategoryId, SpendingCategoryType.CUSTOM, user);

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

Spending spendingAfterMigration = spendingService.readSpending(spendingId).orElseThrow();
Assertions.assertEquals(spendingAfterMigration.getSpendingCustomCategory().getId(), toCategory.getId());
}

@Test
@DisplayName("사용자는 기본 카테고리에서 커스텀 카테고리로 지출내역들을 옮길 수 있다.")
void migrateSpendingsDtoC() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
Spending spending = spendingService.createSpending(SpendingFixture.GENERAL_SPENDING.toSpending(user));

SpendingCustomCategory toCategory = spendingCustomCategoryService.createSpendingCustomCategory(SpendingCustomCategoryFixture.GENERAL_SPENDING_CUSTOM_CATEGORY.toCustomSpendingCategory(user));
Long toCategoryId = toCategory.getId();
Long spendingId = spending.getId();

// when
ResultActions resultActions = performMigrateSpendingsByCategory(1L, SpendingCategoryType.DEFAULT, toCategoryId, SpendingCategoryType.CUSTOM, user);

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

Spending spendingAfterMigration = spendingService.readSpending(spendingId).orElseThrow();
Assertions.assertEquals(spendingAfterMigration.getSpendingCustomCategory().getId(), toCategory.getId());
}

@Test
@DisplayName("사용자는 기본 카테고리에서 기본 카테고리로 지출내역들을 옮길 수 있다.")
void migrateSpendingsDtoD() throws Exception {
// given
User user = userService.createUser(UserFixture.GENERAL_USER.toUser());
Spending spending = spendingService.createSpending(SpendingFixture.GENERAL_SPENDING.toSpending(user));
Long spendingId = spending.getId();
SpendingCategory toCategory = SpendingCategory.TRANSPORTATION;

// when
ResultActions resultActions = performMigrateSpendingsByCategory(1L, SpendingCategoryType.DEFAULT, 2L, SpendingCategoryType.DEFAULT, user);

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

Spending spendingAfterMigration = spendingService.readSpending(spendingId).orElseThrow();
Assertions.assertEquals(spendingAfterMigration.getCategory().icon(), toCategory);
}

private ResultActions performMigrateSpendingsByCategory(Long fromId, SpendingCategoryType fromType, Long toId, SpendingCategoryType toType, User requestUser) throws Exception {
UserDetails userDetails = SecurityUserDetails.from(requestUser);

return mockMvc.perform(MockMvcRequestBuilders.patch("/v2/spending-categories/{fromId}/migration", fromId)
.param("fromType", fromType.toString())
.param("toId", toId.toString())
.param("toType", toType.toString())
.with(user(userDetails))
.contentType("application/json"));
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.api.apis.ledger.dto.SpendingReq;
import kr.co.pennyway.api.common.security.authorization.SpendingCategoryManager;
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;
Expand Down Expand Up @@ -31,6 +32,8 @@ public class SpendingUpdateServiceTest {
private SpendingCustomCategoryService spendingCustomCategoryService;
@Mock
private SpendingService spendingService;
@Mock
private SpendingCategoryManager spendingCategoryManager;

private Spending spending;
private Spending spendingWithCustomCategory;
Expand All @@ -42,7 +45,7 @@ public class SpendingUpdateServiceTest {

@BeforeEach
void setUp() {
spendingUpdateService = new SpendingUpdateService(spendingService, spendingCustomCategoryService);
spendingUpdateService = new SpendingUpdateService(spendingService, spendingCustomCategoryService, spendingCategoryManager);

request = new SpendingReq(10000, -1L, SpendingCategory.FOOD, LocalDate.now(), "소비처", "메모");
requestWithCustomCategory = new SpendingReq(10000, 1L, SpendingCategory.CUSTOM, LocalDate.now(), "소비처", "메모");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,24 @@ public interface SpendingRepository extends ExtendedRepository<Spending, Long>,
@Transactional
@Query("UPDATE Spending s SET s.deletedAt = NOW() where s.id IN :spendingIds AND s.deletedAt IS NULL")
void deleteAllByIdAndDeletedAtNullInQuery(List<Long> spendingIds);

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Spending s SET s.spendingCustomCategory.id = :toCategoryId, s.category = :custom WHERE s.category = :fromCategory AND s.deletedAt IS NULL")
void updateCategoryByCustomCategoryInQuery(SpendingCategory fromCategory, Long toCategoryId, SpendingCategory custom);

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Spending s SET s.category = :toCategory WHERE s.category = :fromCategory AND s.deletedAt IS NULL")
void updateCategoryByCategoryInQuery(SpendingCategory fromCategory, SpendingCategory toCategory);

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Spending s SET s.spendingCustomCategory.id = :toCategoryId WHERE s.spendingCustomCategory.id = :fromCategoryId AND s.deletedAt IS NULL")
void updateCustomCategoryByCustomCategoryInQuery(Long fromCategoryId, Long toCategoryId);

@Modifying(clearAutomatically = true)
@Transactional
@Query("UPDATE Spending s SET s.spendingCustomCategory = null, s.category = :toCategory WHERE s.spendingCustomCategory.id = :fromCategoryId AND s.deletedAt IS NULL")
void updateCustomCategoryByCategoryInQuery(Long fromCategoryId, SpendingCategory toCategory);
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,26 @@ public void deleteSpendingsInQuery(List<Long> spendingIds) {
public long countByUserIdAndIdIn(Long userId, List<Long> spendingIds) {
return spendingRepository.countByUserIdAndIdIn(userId, spendingIds);
}

@Transactional
public void updateCategoryByCustomCategory(SpendingCategory fromCategory, Long toId) {
SpendingCategory custom = SpendingCategory.CUSTOM;
spendingRepository.updateCategoryByCustomCategoryInQuery(fromCategory, toId, custom);
}

@Transactional
public void updateCategoryByCategory(SpendingCategory fromCategory, SpendingCategory toCategory) {

spendingRepository.updateCategoryByCategoryInQuery(fromCategory, toCategory);
}

@Transactional
public void updateCustomCategoryByCustomCategory(Long fromId, Long toId) {
spendingRepository.updateCustomCategoryByCustomCategoryInQuery(fromId, toId);
}

@Transactional
public void updateCustomCategoryByCategory(Long fromId, SpendingCategory toCategory) {
spendingRepository.updateCustomCategoryByCategoryInQuery(fromId, toCategory);
}
}

0 comments on commit 5566c29

Please sign in to comment.