diff --git a/src/docs/asciidoc/coupon/coupon-api.adoc b/src/docs/asciidoc/coupon/coupon-api.adoc index 573b0fc9..be93ff65 100644 --- a/src/docs/asciidoc/coupon/coupon-api.adoc +++ b/src/docs/asciidoc/coupon/coupon-api.adoc @@ -111,7 +111,7 @@ include::{snippets}/coupon-backoffice-controller-docs-test/modify-coupon-request == 쿠폰 현황 통계 -쿠폰 현환 통계 API +쿠폰 현황 통계 API === HttpRequest @@ -121,3 +121,19 @@ include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-tes include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-test/http-response.adoc[] include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-test/response-fields.adoc[] + + +[[Statistics-Revenue]] + +== 일주일 매출 현황 통계 + +최근 일주일 매출 현황 통계 API + +=== HttpRequest + +include::{snippets}/coupon-backoffice-controller-docs-test/revenue-statistics-test/http-request.adoc[] + +=== HttpResponse + +include::{snippets}/coupon-backoffice-controller-docs-test/revenue-statistics-test/http-response.adoc[] +include::{snippets}/coupon-backoffice-controller-docs-test/revenue-statistics-test/response-fields.adoc[] diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/entity/Accommodation.java b/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/entity/Accommodation.java index 5729d1d3..11d95785 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/entity/Accommodation.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/entity/Accommodation.java @@ -15,6 +15,7 @@ import jakarta.persistence.OneToOne; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -84,4 +85,22 @@ public Accommodation( this.images = images; this.rooms = rooms; } + + // 숙소의 ID로 동등 비교를 하기 위해 추가함. + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Accommodation that = (Accommodation) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); + } } diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/config/MockStatisticsInit.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/config/MockStatisticsInit.java new file mode 100644 index 00000000..f2901b7b --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/config/MockStatisticsInit.java @@ -0,0 +1,143 @@ +package com.backoffice.upjuyanolja.domain.coupon.config; + +import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; +import com.backoffice.upjuyanolja.domain.accommodation.exception.AccommodationNotFoundException; +import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository; +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.CouponStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.RevenueStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueTotal; +import com.backoffice.upjuyanolja.domain.coupon.repository.CouponStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueTotalRepository; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.transaction.annotation.Transactional; + + +@Slf4j +@Configuration +@RequiredArgsConstructor +@Transactional +@Profile("prod") +public class MockStatisticsInit { + + private final CouponStatisticsRepository couponStatisticsRepository; + private final RevenueStatisticsRepository revenueStatisticsRepository; + private final RevenueTotalRepository revenueTotalRepository; + private final AccommodationRepository accommodationRepository; + + // Todo: 테스트용. 발표 끝나면 파일 삭제 + @Bean + ApplicationRunner init() { + return new ApplicationRunner() { + @Override + public void run(ApplicationArguments args) throws Exception { + if (!couponStatisticsRepository.existsById(1L)) { + makeCouponStatistics(); + } + if (!revenueStatisticsRepository.existsById(1L)) { + createRevenueStatistics(); + } + } + }; + } + + public void makeCouponStatistics() { + List result = couponStatisticsRepository.createStatistics(); + List statisticsList = new ArrayList<>(); + for (CouponStatisticsInterface statistics : result) { + statisticsList.add(createCouponStatistics(statistics)); + } + couponStatisticsRepository.saveAll(statisticsList); + log.info("쿠폰 통계 생성 성공. 총 {}건.", statisticsList.size()); + } + + private CouponStatistics createCouponStatistics(CouponStatisticsInterface statistics) { + Accommodation accommodation = accommodationRepository.findById(statistics.getId()) + .orElseThrow(AccommodationNotFoundException::new); + return CouponStatistics.builder() + .accommodation(accommodation) + .stock(statistics.getStock()) + .total(statistics.getTotal()) + .used(statistics.getUsed()) + .build(); + } + + public void createRevenueStatistics() { + // 1. 최근 일주일 일자별 통계를 구한다. + LocalDate now = LocalDate.now(ZoneId.of("Asia/Seoul")); + LocalDate endDate = now.minusDays(1); + LocalDate startDate = now.minusWeeks(1).minusDays(1); + + List results = revenueStatisticsRepository + .createRevenueStatistics(startDate, endDate); + + List revenueStatistics = new ArrayList<>(); + + // 2. for-loop 도는 동안 숙소별로 일주일간의 매출 합계를 구한다. + Map revenueTotal = new HashMap<>(); + for (RevenueStatisticsInterface result : results) { + Accommodation accommodation = accommodationRepository.findById(result.getId()) + .orElseThrow(AccommodationNotFoundException::new); + revenueTotal.putIfAbsent(accommodation, new long[2]); + long[] sum = revenueTotal.get(accommodation); + sum[0] += result.getCouponRevenue(); + sum[1] += result.getRegularRevenue(); + revenueStatistics.add(createRevenueStatistics(result, accommodation)); + } + revenueStatisticsRepository.saveAll(revenueStatistics); + log.info("최근 일주일 일자별 매출 통계 생성 성공. 총 {}건.", results.size()); + + // 3. 일주일간의 매출 유형벌 합계로 성장률을 구하고 매출 합계 통계에 저장한다. + List revenueTotals = new ArrayList<>(); + for (var totals: revenueTotal.entrySet()) { + RevenueTotal revenueSum = getRevenueSum(totals); + revenueTotals.add(revenueSum); + } + revenueTotalRepository.saveAll(revenueTotals); + log.info("매출 합계 통계 생성 성공. 총 {}건.", revenueTotals.size()); + } + + private static RevenueTotal getRevenueSum(Entry totals) { + Accommodation accommodation = totals.getKey(); + long[] value = totals.getValue(); + long couponTotal = value[0]; + long regularTotal = value[1]; + long difference = couponTotal - regularTotal; + double growthRate = ((regularTotal + difference) / regularTotal) * 100.0; + log.info("매출 상승 비율: {}", growthRate); + RevenueTotal revenueSum = RevenueTotal.builder() + .accommodation(accommodation) + .couponTotal(couponTotal) + .regularTotal(regularTotal) + .growthRate(growthRate) + .build(); + return revenueSum; + } + + private RevenueStatistics createRevenueStatistics( + RevenueStatisticsInterface result, Accommodation accommodation + ) { + return RevenueStatistics.builder() + .accommodation(accommodation) + .revenueDate(result.getRevenueDate()) + .couponRevenue(result.getCouponRevenue()) + .regularRevenue(result.getRegularRevenue()) + .build(); + } +} + diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/controller/CouponBackofficeController.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/controller/CouponBackofficeController.java index 945df5b1..c654fa68 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/controller/CouponBackofficeController.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/controller/CouponBackofficeController.java @@ -1,12 +1,13 @@ package com.backoffice.upjuyanolja.domain.coupon.controller; -import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse; import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponAddRequest; import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponDeleteRequest; import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponMakeRequest; import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponModifyRequest; import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponMakeViewResponse; import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponManageResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponStatisticsResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.RevenueStatisticsResponse; import com.backoffice.upjuyanolja.domain.coupon.service.CouponBackofficeService; import com.backoffice.upjuyanolja.domain.coupon.service.CouponStatisticsService; import com.backoffice.upjuyanolja.domain.member.service.MemberGetService; @@ -37,6 +38,7 @@ public class CouponBackofficeController { private final CouponBackofficeService couponService; private final CouponStatisticsService couponStatisticsService; private final SecurityUtil securityUtil; + private final MemberGetService memberGetService; @GetMapping("/buy/{accommodationId}") public ResponseEntity responseRoomsView( @@ -131,7 +133,7 @@ public ResponseEntity deleteCoupon( } @GetMapping("/statistics/{accommodationId}") - public ResponseEntity getStatistics( + public ResponseEntity getCouponStatistics( @PathVariable(name = "accommodationId") @Min(1) Long accommodationId ) { long currentMemberId = securityUtil.getCurrentMemberId(); @@ -143,4 +145,17 @@ public ResponseEntity getStatistics( return ResponseEntity.status(HttpStatus.OK).body(result); } + @GetMapping("/revenue/{accommodationId}") + public ResponseEntity getRevenueStatistics( + @PathVariable(name = "accommodationId") @Min(1) Long accommodationId + ) { + long currentMemberId = securityUtil.getCurrentMemberId(); + couponService.validateAccommodationRequest( + accommodationId, currentMemberId); + + String ownerName = memberGetService.getMember(currentMemberId).name(); + RevenueStatisticsResponse result = couponStatisticsService + .getRevenueStatistics(accommodationId, ownerName); + return ResponseEntity.status(HttpStatus.OK).body(result); + } } diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsDto.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsDto.java deleted file mode 100644 index 07e2a087..00000000 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsDto.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.backoffice.upjuyanolja.domain.coupon.dto; - -import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; -import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; -import lombok.Builder; - -@Builder -public record CouponStatisticsDto( - Accommodation accommodation, - long total, - long used, - long stock -) { - - public static CouponStatistics toEntity( - Accommodation accommodation, - long total, - long used, - long stock - ) { - return CouponStatistics.builder() - .accommodation(accommodation) - .total(total) - .used(used) - .stock(stock) - .build(); - } -} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/dto/response/CouponStatisticsResponse.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/CouponStatisticsResponse.java similarity index 88% rename from src/main/java/com/backoffice/upjuyanolja/domain/accommodation/dto/response/CouponStatisticsResponse.java rename to src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/CouponStatisticsResponse.java index 209547f7..5d07cffc 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/accommodation/dto/response/CouponStatisticsResponse.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/CouponStatisticsResponse.java @@ -1,4 +1,4 @@ -package com.backoffice.upjuyanolja.domain.accommodation.dto.response; +package com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; import lombok.Builder; diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueInfo.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueInfo.java new file mode 100644 index 00000000..0cf8e400 --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueInfo.java @@ -0,0 +1,11 @@ +package com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice; + +import lombok.Builder; + +@Builder +public record RevenueInfo( + String revenueDate, + long couponRevenue, + long normalRevenue +) { +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueStatisticsResponse.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueStatisticsResponse.java new file mode 100644 index 00000000..201bff5b --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/response/backoffice/RevenueStatisticsResponse.java @@ -0,0 +1,30 @@ +package com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice; + +import java.util.List; +import java.util.Objects; +import lombok.Builder; + +@Builder +public record RevenueStatisticsResponse( + Long accommodationId, + List revenue, + String couponMessage +) { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RevenueStatisticsResponse that = (RevenueStatisticsResponse) o; + return Objects.equals(accommodationId, that.accommodationId); + } + + @Override + public int hashCode() { + return Objects.hash(accommodationId); + } +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsInterface.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/CouponStatisticsInterface.java similarity index 66% rename from src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsInterface.java rename to src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/CouponStatisticsInterface.java index 79c4850e..0ef33461 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/CouponStatisticsInterface.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/CouponStatisticsInterface.java @@ -1,4 +1,4 @@ -package com.backoffice.upjuyanolja.domain.coupon.dto; +package com.backoffice.upjuyanolja.domain.coupon.dto.statistics; public interface CouponStatisticsInterface { Long getId(); diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/RevenueStatisticsInterface.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/RevenueStatisticsInterface.java new file mode 100644 index 00000000..77187a1b --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/dto/statistics/RevenueStatisticsInterface.java @@ -0,0 +1,14 @@ +package com.backoffice.upjuyanolja.domain.coupon.dto.statistics; + +import java.time.LocalDate; + +public interface RevenueStatisticsInterface { + + long getId(); + + LocalDate getRevenueDate(); + + long getCouponRevenue(); + + long getRegularRevenue(); +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/CouponStatistics.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/CouponStatistics.java index 82f3c6ca..6f72184d 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/CouponStatistics.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/CouponStatistics.java @@ -35,6 +35,7 @@ public class CouponStatistics extends BaseTime { name = "accommodation_id", unique = true, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Comment("숙소 식별자") private Accommodation accommodation; @Setter diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueStatistics.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueStatistics.java new file mode 100644 index 00000000..bd0f7438 --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueStatistics.java @@ -0,0 +1,70 @@ +package com.backoffice.upjuyanolja.domain.coupon.entity; + +import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; +import com.backoffice.upjuyanolja.global.common.entity.BaseTime; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "revenue_statistics", indexes = @Index(name = "idx_accommodation_and_revenue_date", + columnList = "accommodation_id, revenue_date")) +public class RevenueStatistics extends BaseTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("일자별 매출 식별자") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + nullable = false, + name = "accommodation_id", + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Comment("숙소 식별자") + private Accommodation accommodation; + + @Column(nullable = false, name = "revenue_date") + @Comment("매출 일자") + private LocalDate revenueDate; + + @Column(nullable = false, name = "coupon_revenue") + @Comment("쿠폰 사용 매출") + private long couponRevenue; + + @Column(nullable = false, name = "normal revenue") + @Comment("쿠폰 미사용 매출") + private long regularRevenue; + + @Builder + public RevenueStatistics( + Long id, + Accommodation accommodation, + LocalDate revenueDate, + long couponRevenue, + long regularRevenue + ) { + this.id = id; + this.accommodation = accommodation; + this.revenueDate = revenueDate; + this.couponRevenue = couponRevenue; + this.regularRevenue = regularRevenue; + } +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueTotal.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueTotal.java new file mode 100644 index 00000000..9fd8f7bb --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/entity/RevenueTotal.java @@ -0,0 +1,74 @@ +package com.backoffice.upjuyanolja.domain.coupon.entity; + +import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; + +@Getter +@Entity +@NoArgsConstructor +@Table(name = "revenue_total", indexes = @Index(name = "idx_accommodation_id", + columnList = "accommodation_id")) +public class RevenueTotal { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Comment("일주일 간 매출 합계 식별자") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn( + nullable = false, + name = "accommodation_id", + unique = true, + foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Comment("숙소 식별자") + private Accommodation accommodation; + + @Column(nullable = false, name = "coupon_revenue") + @Comment("쿠폰 사용 매출 합계") + private long couponTotal; + + @Column(nullable = false, name = "normal revenue") + @Comment("쿠폰 미사용 매출 합계") + private long regularTotal; + + @Column(name = "growth_rate") + @Comment("매출 상승 비율") + private double growthRate; + + @Builder + public RevenueTotal( + Long id, + Accommodation accommodation, + long couponTotal, + long regularTotal, + double growthRate + ) { + this.id = id; + this.accommodation = accommodation; + this.couponTotal = couponTotal; + this.regularTotal = regularTotal; + this.growthRate = growthRate; + } + + public RevenueTotal updateGrowthRate(double growthRate) { + this.growthRate = growthRate; + return this; + } + +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/CouponStatisticsRepository.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/CouponStatisticsRepository.java index 9c96fe46..79829a48 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/CouponStatisticsRepository.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/CouponStatisticsRepository.java @@ -1,8 +1,9 @@ package com.backoffice.upjuyanolja.domain.coupon.repository; -import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.CouponStatisticsInterface; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -29,4 +30,6 @@ public interface CouponStatisticsRepository extends JpaRepository createStatistics(); + + Optional findByAccommodationId(Long accommodationId); } diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueStatisticsRepository.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueStatisticsRepository.java new file mode 100644 index 00000000..7197cbb1 --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueStatisticsRepository.java @@ -0,0 +1,37 @@ +package com.backoffice.upjuyanolja.domain.coupon.repository; + +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.RevenueStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueStatistics; +import io.lettuce.core.dynamic.annotation.Param; +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RevenueStatisticsRepository extends JpaRepository { + + String revenueSQL = """ + SELECT ac.id AS id, + py.CREATED_AT AS revenueDate, + COALESCE(SUM(CASE WHEN py.DISCOUNT_AMOUNT = 0 THEN py.TOTAL_AMOUNT ELSE 0 END), + 0) AS regularRevenue, + COALESCE(SUM(CASE WHEN py.DISCOUNT_AMOUNT != 0 THEN py.TOTAL_AMOUNT ELSE 0 END), + 0) AS couponRevenue + FROM RESERVATION_ROOM rr + LEFT JOIN RESERVATION rv ON rr.ID = rv.RESERVATION_ROOM_ID + LEFT JOIN PAYMENT py ON rv.PAYMENT_ID = py.ID + LEFT JOIN ROOM rm ON rr.ROOM_ID = rm.ID + LEFT JOIN ACCOMMODATION ac ON rm.ACCOMMODATION_ID = ac.ID + WHERE py.CREATED_AT BETWEEN :startDate AND :endDate + AND rv.STATUS = 'SERVICED' + GROUP BY ac.id, py.CREATED_AT + ORDER BY id, revenueDate + """; + @Query(value = revenueSQL, nativeQuery = true) + List createRevenueStatistics( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + + List findByAccommodationId(Long accommodationId); +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueTotalRepository.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueTotalRepository.java new file mode 100644 index 00000000..2c5587f7 --- /dev/null +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/repository/RevenueTotalRepository.java @@ -0,0 +1,9 @@ +package com.backoffice.upjuyanolja.domain.coupon.repository; + +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueTotal; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RevenueTotalRepository extends JpaRepository { + Optional findByAccommodationId(Long accommodationId); +} diff --git a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/service/CouponStatisticsService.java b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/service/CouponStatisticsService.java index 9a557081..724d9823 100644 --- a/src/main/java/com/backoffice/upjuyanolja/domain/coupon/service/CouponStatisticsService.java +++ b/src/main/java/com/backoffice/upjuyanolja/domain/coupon/service/CouponStatisticsService.java @@ -1,10 +1,22 @@ package com.backoffice.upjuyanolja.domain.coupon.service; -import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse; import com.backoffice.upjuyanolja.domain.accommodation.exception.AccommodationNotFoundException; import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponStatisticsResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.RevenueInfo; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.RevenueStatisticsResponse; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueTotal; import com.backoffice.upjuyanolja.domain.coupon.repository.CouponStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueTotalRepository; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.swing.text.html.Option; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,10 +29,91 @@ public class CouponStatisticsService { private final CouponStatisticsRepository couponStatisticsRepository; + private final RevenueStatisticsRepository revenueStatisticsRepository; + private final RevenueTotalRepository revenueTotalRepository; + // 쿠폰 현황 통계 response public CouponStatisticsResponse getCouponStatistics(Long accommodationId) { - CouponStatistics couponStatistics = couponStatisticsRepository.findById(accommodationId) - .orElseThrow(AccommodationNotFoundException::new); - return CouponStatisticsResponse.from(couponStatistics); + Optional couponStatistics = Optional.ofNullable(couponStatisticsRepository + .findByAccommodationId(accommodationId) + .orElseGet(() -> { + return null; + })); + if (couponStatistics.isEmpty()) { + return null; + } + return CouponStatisticsResponse.from(couponStatistics.get()); + } + + // 최근 일주일 일자별 매출 통계 response + public RevenueStatisticsResponse getRevenueStatistics( + Long accommodationId, String ownerName + ) { + Optional response = Optional.ofNullable( + getRevenueStatisticsResponse(accommodationId, ownerName)); + if (response.isEmpty()) { + return null; + } + return response.get(); + } + + private RevenueStatisticsResponse getRevenueStatisticsResponse( + Long accommodationId, String ownerName + ) { + List query = revenueStatisticsRepository + .findByAccommodationId(accommodationId); + if (query.isEmpty()) { + return null; + } + + List infos = new ArrayList<>(); + for (var result : query) { + infos.add(createRevenueInfo(result)); + } + Optional revenueTotal = Optional.ofNullable(revenueTotalRepository + .findByAccommodationId(accommodationId).orElseGet(() -> { + return null; + })); + if (revenueTotal.isEmpty()) { + return null; + } + + RevenueTotal result = revenueTotal.get(); + String couponMessage = makeCouponMessage(result, ownerName); + return RevenueStatisticsResponse.builder() + .accommodationId(accommodationId) + .revenue(infos) + .couponMessage(couponMessage) + .build(); + } + + private String makeCouponMessage(RevenueTotal revenueTotal, String ownerName) { + int growth = (int) revenueTotal.getGrowthRate(); + if (growth <= 10) { + return new String(); + } + StringBuilder sb = new StringBuilder(); + sb.append(ownerName).append("님, 쿠폰 발급 후 매출이 ").append(growth) + .append("% 늘어났어요!"); + return sb.toString(); + } + + private RevenueStatisticsResponse createRevenueResponse( + Long accommodationId, List query) { + List infos = query.stream() + .map(this::createRevenueInfo).collect(Collectors.toList()); + return RevenueStatisticsResponse.builder() + .accommodationId(accommodationId) + .revenue(infos) + .build(); + } + + private RevenueInfo createRevenueInfo(RevenueStatistics revenueStatistics) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd"); + return RevenueInfo.builder() + .couponRevenue(revenueStatistics.getCouponRevenue()) + .normalRevenue(revenueStatistics.getRegularRevenue()) + .revenueDate(revenueStatistics.getRevenueDate().format(formatter)) + .build(); } } diff --git a/src/main/java/com/backoffice/upjuyanolja/global/config/DummyDevInit.java b/src/main/java/com/backoffice/upjuyanolja/global/config/DummyDevInit.java deleted file mode 100644 index 0608eceb..00000000 --- a/src/main/java/com/backoffice/upjuyanolja/global/config/DummyDevInit.java +++ /dev/null @@ -1,241 +0,0 @@ -package com.backoffice.upjuyanolja.global.config; - -import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; -import com.backoffice.upjuyanolja.domain.accommodation.entity.AccommodationOption; -import com.backoffice.upjuyanolja.domain.accommodation.entity.AccommodationOwnership; -import com.backoffice.upjuyanolja.domain.accommodation.entity.Address; -import com.backoffice.upjuyanolja.domain.accommodation.entity.Category; -import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationOwnershipRepository; -import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository; -import com.backoffice.upjuyanolja.domain.accommodation.repository.CategoryRepository; -import com.backoffice.upjuyanolja.domain.coupon.entity.Coupon; -import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatus; -import com.backoffice.upjuyanolja.domain.coupon.entity.CouponType; -import com.backoffice.upjuyanolja.domain.coupon.entity.DiscountType; -import com.backoffice.upjuyanolja.domain.coupon.repository.CouponRepository; -import com.backoffice.upjuyanolja.domain.member.entity.Authority; -import com.backoffice.upjuyanolja.domain.member.entity.Member; -import com.backoffice.upjuyanolja.domain.member.entity.Owner; -import com.backoffice.upjuyanolja.domain.member.exception.CreateVerificationCodeException; -import com.backoffice.upjuyanolja.domain.member.repository.MemberRepository; -import com.backoffice.upjuyanolja.domain.member.repository.OwnerRepository; -import com.backoffice.upjuyanolja.domain.point.entity.Point; -import com.backoffice.upjuyanolja.domain.point.repository.PointRepository; -import com.backoffice.upjuyanolja.domain.room.entity.Room; -import com.backoffice.upjuyanolja.domain.room.entity.RoomOption; -import com.backoffice.upjuyanolja.domain.room.entity.RoomPrice; -import com.backoffice.upjuyanolja.domain.room.entity.RoomStatus; -import com.backoffice.upjuyanolja.domain.room.repository.RoomRepository; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.YearMonth; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.transaction.annotation.Transactional; - -@Profile("dev") -@Slf4j -@Configuration -@RequiredArgsConstructor -@Transactional -public class DummyDevInit { - - private final MemberRepository memberRepository; - private final AccommodationRepository accommodationRepository; - private final AccommodationOwnershipRepository accommodationOwnershipRepository; - private final CouponRepository couponRepository; - private final RoomRepository roomRepository; - private final CategoryRepository categoryRepository; - private final PointRepository pointRepository; - private final BCryptPasswordEncoder encoder; - private final OwnerRepository ownerRepository; - - @Profile("dev") - @Bean - ApplicationRunner init() { - return new ApplicationRunner() { - @Override - public void run(ApplicationArguments args) throws Exception { - Member member = createMember(1L); - Owner owner = createOwner(1L); - Accommodation accommodation1 = createAccommodation(1L); - createAccommodationOwnership(accommodation1, member); - - List roomIdSet = List.of(1L, 2L, 3L); - List roomNameSet = List.of("스탠다드", "디럭스", "스위트"); - List rooms = createRooms(accommodation1, roomIdSet, roomNameSet); - - List couponIds1 = List.of(1L, 2L, 3L, 4L); - List couponIds2 = List.of(5L, 6L, 7L, 8L); - List couponIds3 = List.of(9L, 10L, 11L, 12L); - createCoupons(couponIds1, rooms.get(0)); - createCoupons(couponIds2, rooms.get(1)); - createCoupons(couponIds3, rooms.get(2)); - - createPoint(1L, member, 200000L); - } - }; - } - - private Member createMember(Long id) { - Member member = Member.builder() - .id(id) - .email("test1@tester.com") - .password(encoder.encode("Qwert12345")) - .name("test") - .phone("010-1234-1234") - .imageUrl( - "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI") - .authority(Authority.ROLE_ADMIN) - .build(); - memberRepository.save(member); - return member; - } - - private Owner createOwner(long ownerId) { - Owner owner = Owner.builder() - .id(ownerId) - .email("test1@tester.com") - .name("test") - .phone("010-1234-5678") - .build(); - ownerRepository.save(owner); - return owner; - } - - private Accommodation createAccommodation(Long accommodationId) { - Accommodation accommodation = Accommodation.builder() - .id(accommodationId) - .name("그랜드 하얏트 제주") - .address(Address.builder() - .address("제주특별자치도 제주시 노형동 925") - .detailAddress("") - .zipCode("63082") - .build()) - .category(createCategory()) - .description( - "63빌딩의 1.8배 규모인 연면적 30만 3737m2, 높이 169m(38층)를 자랑하는 제주 최대 높이, 최대 규모의 랜드마크이다. 제주 고도제한선(55m)보다 높이 위치한 1,600 올스위트 객실, 월드클래스 셰프들이 포진해 있는 14개의 글로벌 레스토랑 & 바, 인피니티 풀을 포함한 8층 야외풀데크, 38층 스카이데크를 비롯해 HAN컬렉션 K패션 쇼핑몰, 2개의 프리미엄 스파, 8개의 연회장 등 라스베이거스, 싱가포르, 마카오에서나 볼 수 있는 세계적인 수준의 복합리조트이다. 제주국제공항에서 차량으로 10분거리(5km)이며 제주의 강남이라고 불리는 신제주 관광 중심지에 위치하고 있다.") - .thumbnail("http://tong.visitkorea.or.kr/cms/resource/83/2876783_image2_1.jpg") - .option(AccommodationOption.builder() - .cooking(false).parking(true).pickup(false).barbecue(false).fitness(true) - .karaoke(false).sauna(false).sports(true).seminar(true) - .build()) - .images(new ArrayList<>()) - .rooms(new ArrayList<>()) - .build(); - accommodationRepository.save(accommodation); - return accommodation; - } - - private Category createCategory() { - Category category = Category.builder() - .id(5L) - .name("TOURIST_HOTEL") - .build(); - categoryRepository.save(category); - return category; - } - - private AccommodationOwnership createAccommodationOwnership( - Accommodation accommodation, Member member - ) { - AccommodationOwnership ownership = AccommodationOwnership.builder() - .id(1L) - .accommodation(accommodation) - .member(member) - .build(); - accommodationOwnershipRepository.save(ownership); - return ownership; - } - - private List createRooms( - Accommodation accommodation, List roomIds, List roomNames - ) { - List rooms = List.of( - createRoom(roomIds.get(0), roomNames.get(0), accommodation), - createRoom(roomIds.get(1), roomNames.get(1), accommodation), - createRoom(roomIds.get(2), roomNames.get(2), accommodation) - ); - roomRepository.saveAll(rooms); - return rooms; - } - - private Room createRoom(Long roomId, String roomName, Accommodation accommodation) { - return Room.builder() - .id(roomId) - .accommodation(accommodation) - .name(roomName) - .defaultCapacity(2) - .maxCapacity(3) - .checkInTime(LocalTime.of(15, 0, 0)) - .checkOutTime(LocalTime.of(11, 0, 0)) - .price(RoomPrice.builder() - .offWeekDaysMinFee(100000) - .offWeekendMinFee(100000) - .peakWeekDaysMinFee(100000) - .peakWeekendMinFee(100000) - .build()) - .amount(858) - .status(RoomStatus.SELLING) - .option(RoomOption.builder() - .airCondition(true) - .tv(true) - .internet(true) - .build()) - .images(new ArrayList<>()) - .build(); - } - - private List createCoupons(List couponIds, Room room) { - List coupons = List.of( - createCoupon( - couponIds.get(0), room, DiscountType.FLAT, CouponStatus.ENABLE, 5000, 20), - createCoupon( - couponIds.get(1), room, DiscountType.RATE, CouponStatus.ENABLE, 10, 20), - createCoupon( - couponIds.get(2), room, DiscountType.FLAT, CouponStatus.SOLD_OUT, 1000, 0), - createCoupon( - couponIds.get(3), room, DiscountType.RATE, CouponStatus.DELETED, 30, 0) - ); - couponRepository.saveAll(coupons); - return coupons; - } - - private Coupon createCoupon( - long couponId, Room room, DiscountType discountType, CouponStatus status, int discount, - int stock - ) { - return Coupon.builder() - .id(couponId) - .room(room) - .couponType(CouponType.ALL_DAYS) - .discountType(discountType) - .couponStatus(status) - .discount(discount) - .endDate(LocalDate.now().plusMonths(1)) - .dayLimit(-1) - .stock(stock) - .build(); - } - - private Point createPoint(Long pointId, Member member, long balance) { - Point point = Point.builder() - .id(pointId) - .member(member) - .totalPointBalance(balance) - .build(); - pointRepository.save(point); - return point; - } -} diff --git a/src/main/java/com/backoffice/upjuyanolja/global/scheduler/CouponStatisticsScheduler.java b/src/main/java/com/backoffice/upjuyanolja/global/scheduler/CouponStatisticsScheduler.java index 8cd4342d..ca832366 100644 --- a/src/main/java/com/backoffice/upjuyanolja/global/scheduler/CouponStatisticsScheduler.java +++ b/src/main/java/com/backoffice/upjuyanolja/global/scheduler/CouponStatisticsScheduler.java @@ -3,12 +3,21 @@ import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; import com.backoffice.upjuyanolja.domain.accommodation.exception.AccommodationNotFoundException; import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository; -import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsDto; -import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.CouponStatisticsInterface; +import com.backoffice.upjuyanolja.domain.coupon.dto.statistics.RevenueStatisticsInterface; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueStatistics; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueTotal; import com.backoffice.upjuyanolja.domain.coupon.repository.CouponStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueStatisticsRepository; +import com.backoffice.upjuyanolja.domain.coupon.repository.RevenueTotalRepository; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.EnableScheduling; @@ -22,6 +31,8 @@ public class CouponStatisticsScheduler { private final CouponStatisticsRepository couponStatisticsRepository; + private final RevenueStatisticsRepository revenueStatisticsRepository; + private final RevenueTotalRepository revenueTotalRepository; private final AccommodationRepository accommodationRepository; // 매일 새벽 2시에 통계 쿼리 실행 @@ -39,12 +50,74 @@ public void makeCouponStatistics() { private CouponStatistics createCouponStatistics(CouponStatisticsInterface statistics) { Accommodation accommodation = accommodationRepository.findById(statistics.getId()) .orElseThrow(AccommodationNotFoundException::new); - return CouponStatisticsDto.toEntity( - accommodation, - statistics.getTotal(), - statistics.getUsed(), - statistics.getStock() - ); + return CouponStatistics.builder() + .accommodation(accommodation) + .stock(statistics.getStock()) + .total(statistics.getTotal()) + .used(statistics.getUsed()) + .build(); } + // startDate: 일주일 전 - 1일 / endDate : 오늘 날짜 - 1일 + @Scheduled(cron = "0 30 2 * * *", zone = "Asia/Seoul") + public void createRevenueStatistics() { + // 1. 최근 일주일 일자별 통계를 구한다. + LocalDate now = LocalDate.now(ZoneId.of("Asia/Seoul")); + LocalDate endDate = now.minusDays(1); + LocalDate startDate = now.minusWeeks(1).minusDays(1); + + List results = revenueStatisticsRepository + .createRevenueStatistics(startDate, endDate); + + List revenueStatistics = new ArrayList<>(); + + // 2. for-loop 도는 동안 숙소별로 일주일간의 매출 합계를 구한다. + Map revenueTotal = new HashMap<>(); + for (RevenueStatisticsInterface result : results) { + Accommodation accommodation = accommodationRepository.findById(result.getId()) + .orElseThrow(AccommodationNotFoundException::new); + revenueTotal.putIfAbsent(accommodation, new long[2]); + long[] sum = revenueTotal.get(accommodation); + sum[0] += result.getCouponRevenue(); + sum[1] += result.getRegularRevenue(); + revenueStatistics.add(createRevenueStatistics(result, accommodation)); + } + revenueStatisticsRepository.saveAll(revenueStatistics); + log.info("최근 일주일 일자별 매출 통계 생성 성공. 총 {}건.", results.size()); + + // 3. 일주일간의 매출 유형벌 합계로 성장률을 구하고 매출 합계 통계에 저장한다. + List revenueTotals = new ArrayList<>(); + for (var totals: revenueTotal.entrySet()) { + RevenueTotal revenueSum = getRevenueSum(totals); + revenueTotals.add(revenueSum); + } + revenueTotalRepository.saveAll(revenueTotals); + log.info("매출 합계 통계 생성 성공. 총 {}건.", revenueTotals.size()); + } + + private RevenueTotal getRevenueSum(Entry totals) { + Accommodation accommodation = totals.getKey(); + long[] value = totals.getValue(); + long couponTotal = value[0]; + long regularTotal = value[1]; + long difference = couponTotal - regularTotal; + double growthRate = ((regularTotal + difference) / regularTotal) * 100.0; + return RevenueTotal.builder() + .accommodation(accommodation) + .couponTotal(couponTotal) + .regularTotal(regularTotal) + .growthRate(growthRate) + .build(); + } + + private RevenueStatistics createRevenueStatistics( + RevenueStatisticsInterface result, Accommodation accommodation + ) { + return RevenueStatistics.builder() + .accommodation(accommodation) + .revenueDate(result.getRevenueDate()) + .couponRevenue(result.getCouponRevenue()) + .regularRevenue(result.getRegularRevenue()) + .build(); + } } diff --git a/src/test/java/com/backoffice/upjuyanolja/domain/coupon/docs/CouponBackofficeControllerDocsTest.java b/src/test/java/com/backoffice/upjuyanolja/domain/coupon/docs/CouponBackofficeControllerDocsTest.java index 21b4806f..d26e6231 100644 --- a/src/test/java/com/backoffice/upjuyanolja/domain/coupon/docs/CouponBackofficeControllerDocsTest.java +++ b/src/test/java/com/backoffice/upjuyanolja/domain/coupon/docs/CouponBackofficeControllerDocsTest.java @@ -1,6 +1,8 @@ package com.backoffice.upjuyanolja.domain.coupon.docs; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.times; @@ -20,7 +22,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponStatisticsResponse; import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; import com.backoffice.upjuyanolja.domain.accommodation.entity.AccommodationOption; import com.backoffice.upjuyanolja.domain.accommodation.entity.Address; @@ -41,12 +43,16 @@ import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponManageResponse; import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponManageRooms; import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponRoomsResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.RevenueInfo; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.RevenueStatisticsResponse; import com.backoffice.upjuyanolja.domain.coupon.entity.Coupon; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatus; import com.backoffice.upjuyanolja.domain.coupon.entity.CouponType; import com.backoffice.upjuyanolja.domain.coupon.entity.DiscountType; +import com.backoffice.upjuyanolja.domain.coupon.entity.RevenueStatistics; import com.backoffice.upjuyanolja.domain.coupon.service.CouponBackofficeService; import com.backoffice.upjuyanolja.domain.coupon.service.CouponStatisticsService; +import com.backoffice.upjuyanolja.domain.member.dto.response.MemberInfoResponse; import com.backoffice.upjuyanolja.domain.member.entity.Authority; import com.backoffice.upjuyanolja.domain.member.entity.Member; import com.backoffice.upjuyanolja.domain.member.service.MemberGetService; @@ -522,13 +528,77 @@ public void couponStatisticsTest() throws Exception { subsectionWithPath("used").type(JsonFieldType.NUMBER) .description("사용 완료 쿠폰"), subsectionWithPath("stock").type(JsonFieldType.NUMBER) - .description("현재 보유 쿠폰")) + .description("현재 보유 쿠폰") + ) )); verify(couponStatisticsService, times(1)) .getCouponStatistics(any(Long.TYPE)); } + @Test + @DisplayName("최근 일주일 일자별 매출 통계 테스트") + @WithMockUser(roles = "ADMIN") + public void revenueStatisticsTest() throws Exception { + // given + MemberInfoResponse memberInfoResponse = MemberInfoResponse.builder() + .memberId(1L) + .email("test@mail.com") + .name("test") + .phoneNumber("010-1234-1234") + .build(); + + given(securityUtil.getCurrentMemberId()).willReturn(1L); + given(memberGetService.getMember(any(Long.TYPE))).willReturn(memberInfoResponse); + String mockName = mockMember.getName(); + + // when & Then + RevenueStatisticsResponse mockResponse = createMockRevenueResponse(); + + given(couponStatisticsService.getRevenueStatistics(any(Long.TYPE), any(String.class))) + .willReturn(mockResponse); + + mockMvc.perform(get("/api/coupons/backoffice/revenue/1")) + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andDo(restDoc.document( + responseFields( + subsectionWithPath("accommodationId").type(JsonFieldType.NUMBER) + .description("숙소 식별자"), + subsectionWithPath("revenue").type(JsonFieldType.ARRAY) + .description("매출 통계 배열"), + subsectionWithPath("revenue[].revenueDate").type(JsonFieldType.STRING) + .description("매출 일자"), + subsectionWithPath("revenue[].couponRevenue").type(JsonFieldType.NUMBER) + .description("쿠폰 사용 매출"), + subsectionWithPath("revenue[].normalRevenue").type(JsonFieldType.NUMBER) + .description("쿠폰 미사용 매출"), + subsectionWithPath("couponMessage").type(JsonFieldType.STRING) + .description("쿠폰 메시지").optional()) + )); + + verify(couponStatisticsService, times(1)) + .getRevenueStatistics(any(Long.TYPE), any(String.class)); + } + + private RevenueStatisticsResponse createMockRevenueResponse() { + List infos = List.of( + new RevenueInfo("1/19", 800000L, 400000L), + new RevenueInfo("1/20", 0L, 300000L), + new RevenueInfo("1/21", 900000L, 200000L), + new RevenueInfo("1/22", 200000L, 100000L), + new RevenueInfo("1/23", 300000L, 150000L), + new RevenueInfo("1/24", 600000L, 400000L), + new RevenueInfo("1/25", 700000L, 500000L), + new RevenueInfo("1/26", 100000L, 100000L) + ); + return RevenueStatisticsResponse.builder() + .accommodationId(1L) + .revenue(infos) + .couponMessage("김업주님. 쿠폰 발급 후 매출이 100% 늘어났어요!") + .build(); + } + private CouponStatisticsResponse createMockStatisticsResponse() { return CouponStatisticsResponse.builder() .accommodationId(1L) @@ -537,6 +607,7 @@ private CouponStatisticsResponse createMockStatisticsResponse() { .stock(200) .build(); } + private List createMockDeleteCoupons() { List deleteInfos1 = List.of( new CouponDeleteInfos(1L), @@ -561,21 +632,27 @@ private List createMockDeleteCoupons() { private List createMockAddCoupons() { List coupons1 = List.of( new CouponAddInfos(1L, CouponStatus.ENABLE, DiscountType.FLAT, 50000, 20, 50, - CouponType.ALL_DAYS, 1000), + CouponType.ALL_DAYS, 1000 + ), new CouponAddInfos(2L, CouponStatus.ENABLE, DiscountType.RATE, 10, 20, 50, - CouponType.ALL_DAYS, 1000) + CouponType.ALL_DAYS, 1000 + ) ); List coupons2 = List.of( new CouponAddInfos(5L, CouponStatus.ENABLE, DiscountType.FLAT, 10000, 20, 50, - CouponType.ALL_DAYS, 1000), + CouponType.ALL_DAYS, 1000 + ), new CouponAddInfos(6L, CouponStatus.ENABLE, DiscountType.RATE, 20, 20, 50, - CouponType.ALL_DAYS, 1000) + CouponType.ALL_DAYS, 1000 + ) ); List coupons3 = List.of( new CouponAddInfos(9L, CouponStatus.ENABLE, DiscountType.FLAT, 30000, 20, 50, - CouponType.ALL_DAYS, 1000), + CouponType.ALL_DAYS, 1000 + ), new CouponAddInfos(10L, CouponStatus.ENABLE, DiscountType.RATE, 50, 20, 50, - CouponType.ALL_DAYS, 1000) + CouponType.ALL_DAYS, 1000 + ) ); List rooms = List.of( new CouponAddRooms(1L, coupons1), diff --git a/src/test/java/com/backoffice/upjuyanolja/domain/coupon/unit/controller/CouponBackofficeControllerTest.java b/src/test/java/com/backoffice/upjuyanolja/domain/coupon/unit/controller/CouponBackofficeControllerTest.java index f1beeacc..fab58cd4 100644 --- a/src/test/java/com/backoffice/upjuyanolja/domain/coupon/unit/controller/CouponBackofficeControllerTest.java +++ b/src/test/java/com/backoffice/upjuyanolja/domain/coupon/unit/controller/CouponBackofficeControllerTest.java @@ -15,7 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse; +import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponStatisticsResponse; import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation; import com.backoffice.upjuyanolja.domain.accommodation.entity.AccommodationOption; import com.backoffice.upjuyanolja.domain.accommodation.entity.Address; diff --git a/src/test/java/com/backoffice/upjuyanolja/httpTest/coupon/backoffice/coupon-geRevenueStatistics.http b/src/test/java/com/backoffice/upjuyanolja/httpTest/coupon/backoffice/coupon-geRevenueStatistics.http new file mode 100644 index 00000000..2beb3e96 --- /dev/null +++ b/src/test/java/com/backoffice/upjuyanolja/httpTest/coupon/backoffice/coupon-geRevenueStatistics.http @@ -0,0 +1,40 @@ +POST {{owner-login-api}} +Content-Type: application/json + +{ + "email": "{{owner1-email}}", + "password": "{{owner1-password}}" +} + +> {% + client.log(response.body.accessToken); + client.global.set("access_token", response.body.accessToken); +%} + +### + +GET http://localhost:8080/api/coupons/backoffice/revenue/1 +Accept: application/json +Authorization: Bearer {{access_token}} + +### + +GET http://localhost:8080/api/coupons/backoffice/revenue/test/1 +Accept: application/json +Authorization: Bearer {{access_token}} + +### + +POST {{member-login-api}} +Content-Type: application/json + +{ + "email": "{{member-email}}", + "password": "{{member-password}}" +} + +> {% + client.log(response.body.data.accessToken); + client.global.set("access_token",response.body.data.accessToken) +%} +