Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api: ✨ 사용자 정의 카테고리 이동 API #125

Merged
merged 21 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a1558c3
feat: controller, usecase 작성
asn6878 Jul 10, 2024
ca2ce38
feat: update service, domain service 작성
asn6878 Jul 10, 2024
9e09890
docs: swagger 작성
asn6878 Jul 10, 2024
7020166
test: 지출 카테고리 수정 테스트 작성
asn6878 Jul 10, 2024
4a34493
fix: controller 파라미터 수정
asn6878 Jul 10, 2024
3505024
fix: spel 표현식 제거
asn6878 Jul 10, 2024
d8a9460
test: 기본 카테고리 이동 테스트 작성
asn6878 Jul 10, 2024
1161d5a
docs: api 문서 이동
asn6878 Jul 21, 2024
68cbad3
refactor: controller 및 usecase를 spending 에서 spendingcategory로 이전
asn6878 Jul 21, 2024
3a11fa3
feat: 기본 카테고리로부터의 이전 기능 추가
asn6878 Jul 21, 2024
96c5d50
feat: 권한검사 추가
asn6878 Jul 21, 2024
c4d4a0c
test: 각케이스별 테스트케이스 작성
asn6878 Jul 28, 2024
7769880
Merge branch 'dev' of https://github.com/CollaBu/pennyway-was into fe…
asn6878 Jul 28, 2024
e148f9b
fix: 권한검사 로직 이동
asn6878 Jul 28, 2024
296ebe4
fix: spendingerrorcode 상수 제거
asn6878 Jul 28, 2024
6142735
fix: service 메서드 접두사 udpate로 수정
asn6878 Jul 28, 2024
96e21eb
fix: 접두사 udpate로 추가 수정
asn6878 Jul 28, 2024
db18259
fix: 불필요한 schema 제거
asn6878 Jul 28, 2024
6604f6d
fix: service 및 repository 메서드명 prefix customcategory로변경
asn6878 Jul 31, 2024
4ae5b16
Merge branch 'dev' into feat/PW-410-migrate-category
asn6878 Jul 31, 2024
a678c43
Merge branch 'feat/PW-410-migrate-category' of https://github.com/Col…
asn6878 Jul 31, 2024
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
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);
}
}
Loading