From 56e2978fcd6913e97160053f2ef093b66b54ee79 Mon Sep 17 00:00:00 2001 From: Hwan0518 Date: Sat, 31 Jan 2026 12:05:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EB=88=84?= =?UTF-8?q?=EC=A0=81=20=EC=B4=9D=20=EC=9D=B8=EC=84=BC=ED=8B=B0=EB=B8=8C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin API: GET /api/v1/incentive/admin/total/{partnerUuid} - Partner API: GET /api/v1/incentive/partner/total (본인 조회) - 단위 테스트 추가 Co-Authored-By: Claude Opus 4.5 --- .../GetIncentiveDataController.java | 40 +++++++++++ .../PartnerTotalIncentiveResponse.java | 27 +++++++ .../querydsl/IncentiveQuerydslRepository.java | 17 +++++ .../IncentiveQueryRepositoryImpl.java | 7 ++ .../port/out/IncentiveQueryRepository.java | 3 + .../service/GetIncentiveDataService.java | 13 ++++ .../GetIncentiveDataServiceUnitTest.java | 71 +++++++++++++++++++ ...IncentiveJobManagementServiceUnitTest.java | 11 +++ 8 files changed, 189 insertions(+) create mode 100644 src/main/java/greenfirst/be/incentive/adapter/in/web/response/PartnerTotalIncentiveResponse.java diff --git a/src/main/java/greenfirst/be/incentive/adapter/in/web/controller/GetIncentiveDataController.java b/src/main/java/greenfirst/be/incentive/adapter/in/web/controller/GetIncentiveDataController.java index 8654bfb..ae910c8 100644 --- a/src/main/java/greenfirst/be/incentive/adapter/in/web/controller/GetIncentiveDataController.java +++ b/src/main/java/greenfirst/be/incentive/adapter/in/web/controller/GetIncentiveDataController.java @@ -7,6 +7,7 @@ import greenfirst.be.incentive.adapter.in.web.request.IncentiveListRequest; import greenfirst.be.incentive.adapter.in.web.request.PartnerIncentiveListRequest; import greenfirst.be.incentive.adapter.in.web.response.IncentiveListResponse; +import greenfirst.be.incentive.adapter.in.web.response.PartnerTotalIncentiveResponse; import greenfirst.be.incentive.application.dto.in.IncentiveListInDto; import greenfirst.be.incentive.application.dto.out.IncentiveListOutDto; import greenfirst.be.incentive.application.service.GetIncentiveDataService; @@ -21,9 +22,13 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.math.BigDecimal; +import java.util.UUID; + /** * 인센티브 조회 컨트롤러 @@ -137,4 +142,39 @@ public ResponseEntity> partnerGetIncentiveLi return ResponseEntity.ok(new BaseResponse<>(response)); } + + // 3. (admin) 특정 파트너의 총 인센티브 조회 + @Operation( + summary = "파트너 총 인센티브 조회 (관리자)", + description = "특정 파트너의 누적 총 인센티브를 조회합니다.", + tags = { "Incentive - Admin" } + ) + @GetMapping("/admin/total/{partnerUuid}") + @PreAuthorize("hasAuthority('ADMIN')") + @SecurityRequirement(name = "Bearer Auth") + public ResponseEntity> adminGetPartnerTotalIncentive( + @PathVariable UUID partnerUuid + ) { + BigDecimal totalIncentive = getIncentiveDataService.getTotalIncentiveByPartnerUuid(partnerUuid); + return ResponseEntity.ok(new BaseResponse<>(PartnerTotalIncentiveResponse.from(totalIncentive))); + } + + + // 4. (partner) 본인의 총 인센티브 조회 + @Operation( + summary = "본인 총 인센티브 조회 (파트너)", + description = "로그인한 파트너 본인의 누적 총 인센티브를 조회합니다.", + tags = { "Incentive - Partner" } + ) + @GetMapping("/partner/total") + @PreAuthorize("hasAnyAuthority('PERSONAL_PARTNER', 'CORPORATE_PARTNER')") + @SecurityRequirement(name = "Bearer Auth") + public ResponseEntity> partnerGetMyTotalIncentive( + @AuthenticationPrincipal CustomUserDetails authentication + ) { + UUID partnerUuid = authentication.getUserUuid(); + BigDecimal totalIncentive = getIncentiveDataService.getTotalIncentiveByPartnerUuid(partnerUuid); + return ResponseEntity.ok(new BaseResponse<>(PartnerTotalIncentiveResponse.from(totalIncentive))); + } + } diff --git a/src/main/java/greenfirst/be/incentive/adapter/in/web/response/PartnerTotalIncentiveResponse.java b/src/main/java/greenfirst/be/incentive/adapter/in/web/response/PartnerTotalIncentiveResponse.java new file mode 100644 index 0000000..6569b29 --- /dev/null +++ b/src/main/java/greenfirst/be/incentive/adapter/in/web/response/PartnerTotalIncentiveResponse.java @@ -0,0 +1,27 @@ +package greenfirst.be.incentive.adapter.in.web.response; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PartnerTotalIncentiveResponse { + + private BigDecimal totalIncentive; + + + public static PartnerTotalIncentiveResponse from(BigDecimal totalIncentive) { + return PartnerTotalIncentiveResponse.builder() + .totalIncentive(totalIncentive != null ? totalIncentive : BigDecimal.ZERO) + .build(); + } + +} diff --git a/src/main/java/greenfirst/be/incentive/adapter/out/persistence/querydsl/IncentiveQuerydslRepository.java b/src/main/java/greenfirst/be/incentive/adapter/out/persistence/querydsl/IncentiveQuerydslRepository.java index 7823f58..134af7a 100644 --- a/src/main/java/greenfirst/be/incentive/adapter/out/persistence/querydsl/IncentiveQuerydslRepository.java +++ b/src/main/java/greenfirst/be/incentive/adapter/out/persistence/querydsl/IncentiveQuerydslRepository.java @@ -136,4 +136,21 @@ public Map getTotalIncentiveByPartnerUuids(List partnerU return result; } + /** + * 단일 파트너의 총 인센티브 합계 조회 (EARN/REVERSAL 포함, 순합) + */ + public BigDecimal getTotalIncentiveByPartnerUuid(UUID partnerUuid) { + if (partnerUuid == null) { + return BigDecimal.ZERO; + } + + BigDecimal result = queryFactory + .select(qIncentive.totalIncentive.sum()) + .from(qIncentive) + .where(qIncentive.partnerUuid.eq(partnerUuid)) + .fetchOne(); + + return result != null ? result : BigDecimal.ZERO; + } + } diff --git a/src/main/java/greenfirst/be/incentive/adapter/out/persistence/repository/IncentiveQueryRepositoryImpl.java b/src/main/java/greenfirst/be/incentive/adapter/out/persistence/repository/IncentiveQueryRepositoryImpl.java index 65ef00c..9752f6e 100644 --- a/src/main/java/greenfirst/be/incentive/adapter/out/persistence/repository/IncentiveQueryRepositoryImpl.java +++ b/src/main/java/greenfirst/be/incentive/adapter/out/persistence/repository/IncentiveQueryRepositoryImpl.java @@ -103,6 +103,13 @@ public Map getTotalIncentiveByPartnerUuids(List partnerU } + // 단일 파트너의 총 인센티브 합계 조회 (EARN/REVERSAL 포함, 순합) + @Override + public BigDecimal getTotalIncentiveByPartnerUuid(UUID partnerUuid) { + return incentiveQuerydslRepository.getTotalIncentiveByPartnerUuid(partnerUuid); + } + + // 이미 REVERSAL이 존재하는지 확인 (idempotency) @Override public boolean existsByReversalOfIncentiveId(Long originalIncentiveId) { diff --git a/src/main/java/greenfirst/be/incentive/application/port/out/IncentiveQueryRepository.java b/src/main/java/greenfirst/be/incentive/application/port/out/IncentiveQueryRepository.java index cf3f95e..f17684d 100644 --- a/src/main/java/greenfirst/be/incentive/application/port/out/IncentiveQueryRepository.java +++ b/src/main/java/greenfirst/be/incentive/application/port/out/IncentiveQueryRepository.java @@ -39,6 +39,9 @@ public interface IncentiveQueryRepository { // 파트너 UUID 목록별 총 인센티브 합계 조회 (EARN/REVERSAL 포함, 순합) Map getTotalIncentiveByPartnerUuids(List partnerUuids); + // 단일 파트너의 총 인센티브 합계 조회 (EARN/REVERSAL 포함, 순합) + BigDecimal getTotalIncentiveByPartnerUuid(UUID partnerUuid); + // 이미 REVERSAL이 존재하는지 확인 (idempotency) boolean existsByReversalOfIncentiveId(Long originalIncentiveId); diff --git a/src/main/java/greenfirst/be/incentive/application/service/GetIncentiveDataService.java b/src/main/java/greenfirst/be/incentive/application/service/GetIncentiveDataService.java index fd2b777..1c8a1f2 100644 --- a/src/main/java/greenfirst/be/incentive/application/service/GetIncentiveDataService.java +++ b/src/main/java/greenfirst/be/incentive/application/service/GetIncentiveDataService.java @@ -10,6 +10,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.util.UUID; + /** * 인센티브 조회 서비스 @@ -37,4 +40,14 @@ public IncentiveListOutDto getIncentiveList(IncentiveListInDto inDto) { return incentiveQueryRepository.getIncentiveList(inDto); } + + /** + * 파트너의 총 인센티브 조회 + * - 하위 추천인들을 통해 발생한 인센티브의 총합 (EARN + REVERSAL 순합) + */ + @Transactional(readOnly = true) + public BigDecimal getTotalIncentiveByPartnerUuid(UUID partnerUuid) { + return incentiveQueryRepository.getTotalIncentiveByPartnerUuid(partnerUuid); + } + } diff --git a/src/test/java/greenfirst/be/incentive/application/GetIncentiveDataServiceUnitTest.java b/src/test/java/greenfirst/be/incentive/application/GetIncentiveDataServiceUnitTest.java index 7941cc3..4f665de 100644 --- a/src/test/java/greenfirst/be/incentive/application/GetIncentiveDataServiceUnitTest.java +++ b/src/test/java/greenfirst/be/incentive/application/GetIncentiveDataServiceUnitTest.java @@ -110,4 +110,75 @@ void getIncentiveList_returnsRepositoryResult() { } } + + @Nested + @DisplayName("getTotalIncentiveByPartnerUuid - 파트너 총 인센티브 조회") + class GetTotalIncentiveByPartnerUuidTest { + + @Test + @DisplayName("성공: 파트너 UUID로 총 인센티브 합계를 조회한다") + void getTotalIncentiveByPartnerUuid_returnsTotalIncentive() { + // given + UUID partnerUuid = UUID.randomUUID(); + BigDecimal expected = new BigDecimal("12345.67"); + given(incentiveQueryRepository.getTotalIncentiveByPartnerUuid(partnerUuid)) + .willReturn(expected); + + // when + BigDecimal result = getIncentiveDataService.getTotalIncentiveByPartnerUuid(partnerUuid); + + // then + assertThat(result).isEqualTo(expected); + verify(incentiveQueryRepository).getTotalIncentiveByPartnerUuid(partnerUuid); + } + + @Test + @DisplayName("성공: 인센티브가 없는 파트너는 ZERO를 반환한다") + void getTotalIncentiveByPartnerUuid_returnsZeroWhenNoIncentive() { + // given + UUID partnerUuid = UUID.randomUUID(); + given(incentiveQueryRepository.getTotalIncentiveByPartnerUuid(partnerUuid)) + .willReturn(BigDecimal.ZERO); + + // when + BigDecimal result = getIncentiveDataService.getTotalIncentiveByPartnerUuid(partnerUuid); + + // then + assertThat(result).isEqualTo(BigDecimal.ZERO); + verify(incentiveQueryRepository).getTotalIncentiveByPartnerUuid(partnerUuid); + } + + @Test + @DisplayName("성공: 음수 인센티브(REVERSAL 합계가 더 큰 경우)도 그대로 반환한다") + void getTotalIncentiveByPartnerUuid_returnsNegativeWhenReversalExceedsEarn() { + // given + UUID partnerUuid = UUID.randomUUID(); + BigDecimal expected = new BigDecimal("-500.00"); + given(incentiveQueryRepository.getTotalIncentiveByPartnerUuid(partnerUuid)) + .willReturn(expected); + + // when + BigDecimal result = getIncentiveDataService.getTotalIncentiveByPartnerUuid(partnerUuid); + + // then + assertThat(result).isEqualTo(expected); + verify(incentiveQueryRepository).getTotalIncentiveByPartnerUuid(partnerUuid); + } + + @Test + @DisplayName("성공: null UUID 입력 시 ZERO를 반환한다") + void getTotalIncentiveByPartnerUuid_returnsZeroForNullUuid() { + // given + given(incentiveQueryRepository.getTotalIncentiveByPartnerUuid(null)) + .willReturn(BigDecimal.ZERO); + + // when + BigDecimal result = getIncentiveDataService.getTotalIncentiveByPartnerUuid(null); + + // then + assertThat(result).isEqualTo(BigDecimal.ZERO); + verify(incentiveQueryRepository).getTotalIncentiveByPartnerUuid(null); + } + } + } diff --git a/src/test/java/greenfirst/be/incentive/application/IncentiveJobManagementServiceUnitTest.java b/src/test/java/greenfirst/be/incentive/application/IncentiveJobManagementServiceUnitTest.java index 178e8f6..95eff2f 100644 --- a/src/test/java/greenfirst/be/incentive/application/IncentiveJobManagementServiceUnitTest.java +++ b/src/test/java/greenfirst/be/incentive/application/IncentiveJobManagementServiceUnitTest.java @@ -514,6 +514,17 @@ public Map getTotalIncentiveByPartnerUuids(List partnerU return Map.of(); } + @Override + public BigDecimal getTotalIncentiveByPartnerUuid(UUID partnerUuid) { + if (partnerUuid == null) { + return BigDecimal.ZERO; + } + return storage.values().stream() + .filter(incentive -> Objects.equals(incentive.getPartnerUuid(), partnerUuid)) + .map(Incentive::getTotalIncentive) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + @Override public boolean existsByReversalOfIncentiveId(Long originalIncentiveId) { return storage.values().stream()