diff --git a/Server/src/docs/asciidoc/snippets/adjustment/adjustment.adoc b/Server/src/docs/asciidoc/snippets/adjustment/adjustment.adoc index 3f528827..f8f66b50 100644 --- a/Server/src/docs/asciidoc/snippets/adjustment/adjustment.adoc +++ b/Server/src/docs/asciidoc/snippets/adjustment/adjustment.adoc @@ -12,7 +12,7 @@ IMPORTANT: month 를 null 로 주면 연도별 조회, year 을 null 로 주면 전체 조회 [[Adjustment-list]] -== 비디오별 정산 내역 +== 비디오별 정산 내역 페이징 === HTTP Request include::{snippets}/adjustment/adjustment/http-request.adoc[] ==== Request Headers diff --git a/Server/src/docs/asciidoc/snippets/adjustment/videoadjustment.adoc b/Server/src/docs/asciidoc/snippets/adjustment/videoadjustment.adoc new file mode 100644 index 00000000..09916d4f --- /dev/null +++ b/Server/src/docs/asciidoc/snippets/adjustment/videoadjustment.adoc @@ -0,0 +1,24 @@ +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: +:docinfo: shared-head + +[[Video-Adjustment]] += 비디오별 정산 내역 API + +IMPORTANT: month 를 null 로 주면 연도별 조회, year 을 null 로 주면 전체 조회 + +== 비디오별 정산 내역 비율 +=== HTTP Request +include::{snippets}/adjustment/calculatevideorate/http-request.adoc[] +==== Request Headers +include::{snippets}/adjustment/calculatevideorate/request-headers.adoc[] +==== Request Query Parameters +include::{snippets}/adjustment/calculatevideorate/request-parameters.adoc[] +=== HTTP Response +include::{snippets}/adjustment/calculatevideorate/http-response.adoc[] +==== Response Fields +include::{snippets}/adjustment/calculatevideorate/response-fields.adoc[] \ No newline at end of file diff --git a/Server/src/main/java/com/server/domain/adjustment/controller/AdjustmentController.java b/Server/src/main/java/com/server/domain/adjustment/controller/AdjustmentController.java index fde04e2b..9a41b5a1 100644 --- a/Server/src/main/java/com/server/domain/adjustment/controller/AdjustmentController.java +++ b/Server/src/main/java/com/server/domain/adjustment/controller/AdjustmentController.java @@ -4,6 +4,7 @@ import com.server.domain.adjustment.service.AdjustmentService; import com.server.domain.adjustment.service.dto.response.AccountResponse; import com.server.domain.adjustment.service.dto.response.ToTalAdjustmentResponse; +import com.server.domain.adjustment.service.dto.response.VideoAdjustmentResponse; import com.server.domain.order.controller.dto.request.AdjustmentSort; import com.server.domain.adjustment.service.dto.response.AdjustmentResponse; import com.server.global.annotation.LoginId; @@ -21,6 +22,7 @@ import javax.validation.constraints.Min; import javax.validation.constraints.Positive; import java.net.URI; +import java.util.List; @RestController @RequestMapping("/adjustments") @@ -46,7 +48,20 @@ public ResponseEntity> adjustment( Page response = adjustmentService.adjustment(loginMemberId, page - 1, size, month, year, sort.getSort()); - return ResponseEntity.ok(ApiPageResponse.ok(response, getAdjustmentMessage(month, year))); + return ResponseEntity.ok(ApiPageResponse.ok(response, getAdjustmentMessage(month, year) + " 정산 내역")); + } + + @GetMapping("/videos") + public ResponseEntity>> calculateVideoRate( + @RequestParam(required = false) @Min(value = 1) @Max(value = 12) Integer month, + @RequestParam(required = false) @Min(value = 2020) Integer year, + @LoginId Long loginMemberId) { + + checkValidDate(month, year); + + List total = adjustmentService.calculateVideoRate(loginMemberId, month, year); + + return ResponseEntity.ok(ApiSingleResponse.ok(total, getAdjustmentMessage(month, year) + " 비디오 정산 내역")); } @GetMapping("/total-adjustment") @@ -59,7 +74,7 @@ public ResponseEntity> calculateAmoun ToTalAdjustmentResponse total = adjustmentService.totalAdjustment(loginMemberId, month, year); - return ResponseEntity.ok(ApiSingleResponse.ok(total, getAdjustmentMessage(month, year))); + return ResponseEntity.ok(ApiSingleResponse.ok(total, getAdjustmentMessage(month, year) + " 정산 내역")); } @GetMapping("/account") @@ -91,13 +106,13 @@ private void checkValidDate(Integer month, Integer year) { private String getAdjustmentMessage(Integer month, Integer year) { if(month == null && year == null) { - return "전체 정산 내역"; + return "전체"; } if(month != null && year != null) { - return year + "년 " + month + "월 정산 내역"; + return year + "년 " + month + "월"; } - return year + "년 정산 내역"; + return year + "년"; } } diff --git a/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryCustom.java b/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryCustom.java index f990e89c..d6f19d35 100644 --- a/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryCustom.java +++ b/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryCustom.java @@ -2,6 +2,7 @@ import com.server.domain.adjustment.domain.Adjustment; import com.server.domain.adjustment.repository.dto.AdjustmentData; +import com.server.domain.adjustment.repository.dto.VideoAdjustmentData; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -14,4 +15,6 @@ public interface AdjustmentRepositoryCustom { Integer calculateAmount(Long memberId, Integer month, Integer year); List findMonthlyData(Long memberId, Integer year); + + List calculateVideo(Long memberId, Integer month, Integer year); } diff --git a/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryImpl.java b/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryImpl.java index b6953263..79f72690 100644 --- a/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryImpl.java +++ b/Server/src/main/java/com/server/domain/adjustment/repository/AdjustmentRepositoryImpl.java @@ -1,10 +1,14 @@ package com.server.domain.adjustment.repository; +import com.querydsl.core.types.Predicate; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.server.domain.adjustment.domain.Adjustment; import com.server.domain.adjustment.repository.dto.AdjustmentData; +import com.server.domain.adjustment.repository.dto.QVideoAdjustmentData; +import com.server.domain.adjustment.repository.dto.VideoAdjustmentData; +import com.server.domain.order.entity.OrderStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -15,6 +19,7 @@ import java.util.stream.Collectors; import static com.server.domain.adjustment.domain.QAdjustment.*; +import static com.server.domain.order.entity.QOrder.order; import static com.server.domain.order.entity.QOrderVideo.orderVideo; import static com.server.domain.video.entity.QVideo.video; @@ -64,7 +69,7 @@ public Page findByPeriod(Long memberId, Pageable pageable, Integ JPAQuery countQuery = queryFactory.select(video.count()) .from(video) .join(video.orderVideos, orderVideo) - .where(video.channel.channelId.eq(memberId)); + .where(eqMemberId(memberId)); return new PageImpl<>(videoReportDatas, pageable, countQuery.fetchOne()); } @@ -98,6 +103,48 @@ public List findMonthlyData(Long memberId, Integer year) { .fetch(); } + @Override + public List calculateVideo(Long memberId, Integer month, Integer year) { + + return queryFactory.select( + new QVideoAdjustmentData( + video.videoId, + video.videoName, + orderVideo.price.sum() + ) + ) + .from(video) + .join(video.orderVideos, orderVideo) + .join(orderVideo.order, order) + .where(getCompletedOrder(), + eqMemberId(memberId), + eqOrderYear(year), + eqOrderMonth(month) + ) + .groupBy(video.videoId) + .fetch(); + } + + private Predicate eqOrderYear(Integer year) { + if(year == null) return null; + + return order.completedDate.year().eq(year); + } + + private Predicate eqOrderMonth(Integer month) { + if(month == null) return null; + + return order.completedDate.month().eq(month); + } + + private BooleanExpression eqMemberId(Long memberId) { + return video.channel.channelId.eq(memberId); + } + + private BooleanExpression getCompletedOrder() { + return orderVideo.orderStatus.eq(OrderStatus.COMPLETED); + } + private BooleanExpression YearEq(Integer year) { if(year == null) return null; diff --git a/Server/src/main/java/com/server/domain/adjustment/repository/dto/VideoAdjustmentData.java b/Server/src/main/java/com/server/domain/adjustment/repository/dto/VideoAdjustmentData.java new file mode 100644 index 00000000..73786084 --- /dev/null +++ b/Server/src/main/java/com/server/domain/adjustment/repository/dto/VideoAdjustmentData.java @@ -0,0 +1,21 @@ +package com.server.domain.adjustment.repository.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class VideoAdjustmentData { + + private Long videoId; + private String videoName; + private Integer amount; + + @QueryProjection + public VideoAdjustmentData(Long videoId, String videoName, Integer amount) { + this.videoId = videoId; + this.videoName = videoName; + this.amount = amount; + } +} diff --git a/Server/src/main/java/com/server/domain/adjustment/service/AdjustmentService.java b/Server/src/main/java/com/server/domain/adjustment/service/AdjustmentService.java index fe2a5f49..cc1de9c5 100644 --- a/Server/src/main/java/com/server/domain/adjustment/service/AdjustmentService.java +++ b/Server/src/main/java/com/server/domain/adjustment/service/AdjustmentService.java @@ -6,11 +6,9 @@ import com.server.domain.adjustment.domain.AdjustmentStatus; import com.server.domain.adjustment.repository.AdjustmentRepository; import com.server.domain.adjustment.repository.dto.AdjustmentData; +import com.server.domain.adjustment.repository.dto.VideoAdjustmentData; import com.server.domain.adjustment.service.dto.request.AccountUpdateServiceRequest; -import com.server.domain.adjustment.service.dto.response.AccountResponse; -import com.server.domain.adjustment.service.dto.response.AdjustmentResponse; -import com.server.domain.adjustment.service.dto.response.MonthAdjustmentResponse; -import com.server.domain.adjustment.service.dto.response.ToTalAdjustmentResponse; +import com.server.domain.adjustment.service.dto.response.*; import com.server.domain.member.entity.Member; import com.server.domain.member.repository.MemberRepository; import com.server.global.exception.businessexception.memberexception.MemberNotFoundException; @@ -151,6 +149,7 @@ public AccountResponse getAccount(Long loginMemberId) { return AccountResponse.of(account); } + @Transactional public void updateAccount(Long loginMemberId, AccountUpdateServiceRequest request) { Account account = getAccountOrNull(loginMemberId); @@ -162,7 +161,15 @@ public void updateAccount(Long loginMemberId, AccountUpdateServiceRequest reques }else { account.updateAccount(request.getName(), request.getAccount(), request.getBank()); } + } + + public List calculateVideoRate(Long loginMemberId, Integer month, Integer year) { + + List datas = adjustmentRepository.calculateVideo(loginMemberId, month, year); + + int total = datas.stream().map(VideoAdjustmentData::getAmount).mapToInt(Integer::intValue).sum(); + return datas.stream().map(data -> VideoAdjustmentResponse.of(data, total)).collect(Collectors.toList()); } private Account getAccountOrNull(Long loginMemberId) { diff --git a/Server/src/main/java/com/server/domain/adjustment/service/dto/response/VideoAdjustmentResponse.java b/Server/src/main/java/com/server/domain/adjustment/service/dto/response/VideoAdjustmentResponse.java new file mode 100644 index 00000000..7581a1c1 --- /dev/null +++ b/Server/src/main/java/com/server/domain/adjustment/service/dto/response/VideoAdjustmentResponse.java @@ -0,0 +1,29 @@ +package com.server.domain.adjustment.service.dto.response; + +import com.server.domain.adjustment.repository.dto.VideoAdjustmentData; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@AllArgsConstructor +@Builder +@Getter +public class VideoAdjustmentResponse { + + private Long videoId; + private String videoName; + private Integer amount; + private Float portion; + + public static VideoAdjustmentResponse of(VideoAdjustmentData data, int total) { + + float portion = ((float) (data.getAmount())) / total; + + return VideoAdjustmentResponse.builder() + .videoId(data.getVideoId()) + .videoName(data.getVideoName()) + .amount(data.getAmount()) + .portion(portion) + .build(); + } +} diff --git a/Server/src/test/java/com/server/domain/adjustment/controller/AdjustmentControllerTest.java b/Server/src/test/java/com/server/domain/adjustment/controller/AdjustmentControllerTest.java index 439d6eb0..d8da2c6d 100644 --- a/Server/src/test/java/com/server/domain/adjustment/controller/AdjustmentControllerTest.java +++ b/Server/src/test/java/com/server/domain/adjustment/controller/AdjustmentControllerTest.java @@ -2,10 +2,7 @@ import com.server.domain.adjustment.controller.dto.request.AccountUpdateApiRequest; import com.server.domain.adjustment.domain.AdjustmentStatus; -import com.server.domain.adjustment.service.dto.response.AccountResponse; -import com.server.domain.adjustment.service.dto.response.AdjustmentResponse; -import com.server.domain.adjustment.service.dto.response.MonthAdjustmentResponse; -import com.server.domain.adjustment.service.dto.response.ToTalAdjustmentResponse; +import com.server.domain.adjustment.service.dto.response.*; import com.server.domain.order.controller.dto.request.AdjustmentSort; import com.server.global.reponse.ApiPageResponse; import com.server.global.reponse.ApiSingleResponse; @@ -24,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.*; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -293,7 +291,7 @@ void updateAccount() throws Exception { ResultActions actions = mockMvc.perform( put(BASE_URL + "/account") .header(AUTHORIZATION, TOKEN) - .contentType(MediaType.APPLICATION_JSON) + .contentType(APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) ); @@ -317,8 +315,56 @@ void updateAccount() throws Exception { ) ) ); + } + + @Test + @DisplayName("월별/연도별 비디오별 정산 내역 API") + void calculateVideoRate() throws Exception { + //given + int size = 5; + int year = 2023; + int month = 9; + List response = createVideoAdjustmentResponse(size); + + given(adjustmentService.calculateVideoRate(anyLong(), anyInt(), anyInt())).willReturn(response); + String apiResponse = objectMapper.writeValueAsString(ApiSingleResponse.ok(response, year + "년 " + month + "월 비디오 정산 내역")); + //when + ResultActions actions = mockMvc.perform( + get(BASE_URL + "/videos") + .param("year", String.valueOf(year)) + .param("month", String.valueOf(month)) + .header(AUTHORIZATION, TOKEN) + .accept(APPLICATION_JSON) + ); + + //then + actions + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string(apiResponse)) + ; + + //restdocs + actions.andDo( + documentHandler.document( + requestHeaders( + headerWithName(AUTHORIZATION).description("액세스 토큰") + ), + requestParameters( + parameterWithName("month").description("정산 월").optional(), + parameterWithName("year").description("정산 년도").optional() + ), + singleResponseFields( + fieldWithPath("data").description("정산 내역"), + fieldWithPath("data[].videoId").description("비디오 ID"), + fieldWithPath("data[].videoName").description("비디오 이름"), + fieldWithPath("data[].amount").description("해당 기간 정산 금액"), + fieldWithPath("data[].portion").description("해당 기간 판매 비율") + ) + ) + ); } private ToTalAdjustmentResponse createToTalAdjustmentResponse(Integer month, Integer year) { @@ -367,4 +413,21 @@ private List createAdjustmentResponse(int size) { return responses; } + + private List createVideoAdjustmentResponse(int size) { + + List responses = new ArrayList<>(); + + for(int i = 1; i <= size; i++) { + VideoAdjustmentResponse response = VideoAdjustmentResponse.builder() + .videoId((long) i) + .videoName("videoName") + .amount(10000) + .portion(0.2f) + .build(); + + responses.add(response); + } + return responses; + } } \ No newline at end of file diff --git a/Server/src/test/java/com/server/domain/adjustment/repository/AdjustmentRepositoryTest.java b/Server/src/test/java/com/server/domain/adjustment/repository/AdjustmentRepositoryTest.java index 2075571c..037106d1 100644 --- a/Server/src/test/java/com/server/domain/adjustment/repository/AdjustmentRepositoryTest.java +++ b/Server/src/test/java/com/server/domain/adjustment/repository/AdjustmentRepositoryTest.java @@ -4,22 +4,30 @@ import com.server.domain.adjustment.domain.Adjustment; import com.server.domain.adjustment.domain.AdjustmentStatus; import com.server.domain.adjustment.repository.dto.AdjustmentData; +import com.server.domain.adjustment.repository.dto.VideoAdjustmentData; import com.server.domain.member.entity.Member; import com.server.domain.order.entity.Order; +import com.server.domain.order.entity.OrderVideo; import com.server.domain.order.repository.OrderRepository; import com.server.domain.video.entity.Video; import com.server.global.testhelper.RepositoryTest; +import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import java.time.LocalDateTime; +import java.util.Collection; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.*; +import static org.junit.jupiter.api.DynamicTest.*; class AdjustmentRepositoryTest extends RepositoryTest { @@ -138,4 +146,178 @@ void findMonthlyDataTotal() { adjustment4.getAdjustmentId()); } + + @TestFactory + @DisplayName("해당 월, 해당 연도의 자신의 비디오 판매 금액을 모두 조회한다.") + Collection calculateVideo() { + //given + Member owner1 = createMemberWithChannel(); + Video video1 = createAndSaveVideo(owner1.getChannel()); + Video video2 = createAndSaveVideo(owner1.getChannel()); + Video video3 = createAndSaveVideo(owner1.getChannel()); + + Member buyer1 = createMemberWithChannel(); + buyer1.addReward(10000); + Member buyer2 = createMemberWithChannel(); + buyer2.addReward(10000); + + Order order1 = createAndSaveOrder(buyer1, List.of(video1)); // 8월에 1 주문 + order1.completeOrder(LocalDateTime.of(2023, 8, 1, 0, 0), "paymentKey"); + + Order order2 = createAndSaveOrder(buyer1, List.of(video2, video3)); //9월에 2, 3 주문하고 2 취소 + order2.completeOrder(LocalDateTime.of(2023, 9, 1, 0, 0), "paymentKey"); + OrderVideo orderVideo = order2.getOrderVideos().stream().filter(v -> v.getVideo().getVideoId().equals(video2.getVideoId())).findFirst().get(); + orderVideo.cancel(); + + Order order3 = createAndSaveOrder(buyer2, List.of(video2)); // 9월에 2 주문 + order3.completeOrder(LocalDateTime.of(2023, 9, 1, 0, 0), "paymentKey"); + + Order order4 = createAndSaveOrder(buyer2, List.of(video1)); // 8월에 1 주문 + order4.completeOrder(LocalDateTime.of(2023, 8, 1, 0, 0), "paymentKey"); + + Order order5 = createAndSaveOrder(buyer2, List.of(video3)); // 2022 9월에 3 주문 + order5.completeOrder(LocalDateTime.of(2022, 9, 1, 0, 0), "paymentKey"); + + em.flush(); + em.clear(); + + return List.of( + dynamicTest("2023년 9월 조회 시 2, 3 정산 내역이 나온다.", ()-> { + //given + Integer year = 2023; + Integer month = 9; + + int video2Amount = video2.getPrice(); + int video3Amount = video3.getPrice(); + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(2) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video2.getVideoId(), video2Amount), + tuple(video3.getVideoId(), video3Amount) + ); + }), + dynamicTest("2023년 8월 조회 시 1 정산 내역이 나온다.", ()-> { + //given + Integer year = 2023; + Integer month = 8; + + int video1Amount = video1.getPrice() * 2; + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(1) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video1.getVideoId(), video1Amount) + ); + }), + dynamicTest("2023년 전체 조회 시 1, 2, 3 정산 내역이 나온다.", ()-> { + //given + Integer year = 2023; + Integer month = null; + + int video1Amount = video1.getPrice() * 2; + int video2Amount = video2.getPrice(); + int video3Amount = video3.getPrice(); + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(3) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video1.getVideoId(), video1Amount), + tuple(video2.getVideoId(), video2Amount), + tuple(video3.getVideoId(), video3Amount) + ); + }), + dynamicTest("2022년 전체 조회 시 3 정산 내역이 나온다.", ()-> { + //given + Integer year = 2022; + Integer month = null; + + int video3Amount = video3.getPrice(); + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(1) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video3.getVideoId(), video3Amount) + ); + }), + dynamicTest("2022년 9월 조회 시 3 정산 내역이 나온다.", ()-> { + //given + Integer year = 2022; + Integer month = 9; + + int video3Amount = video3.getPrice(); + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(1) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video3.getVideoId(), video3Amount) + ); + }), + dynamicTest("2023년 7월 조회 시 정산내역이 나오지 않는다.", ()-> { + //given + Integer year = 2022; + Integer month = 7; + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(0); + }), + dynamicTest("전체 조회 시 1, 2, 3 의 전체 내역이 나온다.", ()-> { + //given + Integer year = null; + Integer month = null; + + int video1Amount = video1.getPrice() * 2; + int video2Amount = video2.getPrice(); + int video3Amount = video3.getPrice() * 2; + + //when + List data = adjustmentRepository.calculateVideo(owner1.getMemberId(), month, year); + + //then + assertThat(data).hasSize(3) + .extracting("videoId", "amount") + .containsExactlyInAnyOrder( + tuple(video1.getVideoId(), video1Amount), + tuple(video2.getVideoId(), video2Amount), + tuple(video3.getVideoId(), video3Amount) + ); + }) + + + + + + + + + + + + + ); + + } } \ No newline at end of file