diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 8b85370..3f40875 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -44,7 +44,8 @@ public class HandlerConfig { StatisticUserDynamicHandler statisticUserDynamicHandler, StatisticProtectedFilesHandler statisticProtectedFilesHandler, StatisticViolationHandler statisticViolationHandler, - StatisticSubscriberHandler statisticSubscriberHandler + StatisticSubscriberHandler statisticSubscriberHandler, + StatisticTokenHandler statisticTokenHandler ) { Map map = new HashMap<>(); @@ -81,6 +82,7 @@ public class HandlerConfig { map.put(30022, statisticProtectedFilesHandler); map.put(30023, statisticViolationHandler); map.put(30024, statisticSubscriberHandler); + map.put(30025, statisticTokenHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java new file mode 100644 index 0000000..3623860 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticRequest.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.dto.statistic; + +import lombok.Data; + +@Data +public class TokenStatisticRequest { +} diff --git a/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java new file mode 100644 index 0000000..1a2474c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/statistic/TokenStatisticResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.statistic; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TokenStatisticResponse { + + @JsonProperty("total_bought") + private Long totalBought; + + @JsonProperty("total_spent") + private Long totalSpent; + + @JsonProperty("bought_per_user") + private Percentiles boughtPerUser; + + @JsonProperty("spent_per_user") + private Percentiles spentPerUser; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Percentiles { + @JsonProperty("p5") + private Long p5; + + @JsonProperty("p50") + private Long p50; + + @JsonProperty("p95") + private Long p95; + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java b/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java new file mode 100644 index 0000000..90232e3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/StatisticTokenHandler.java @@ -0,0 +1,29 @@ +package ru.soune.nocopy.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.statistic.TokenStatisticResponse; +import ru.soune.nocopy.service.statistic.TokenStatisticService; + +@Component +@RequiredArgsConstructor +@Slf4j +public class StatisticTokenHandler implements RequestHandler { + private final TokenStatisticService statisticService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + TokenStatisticResponse statistics = statisticService.getTokenStatistics(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + statistics + ); + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java index d599c4b..93456d7 100644 --- a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.payment.Payment; @@ -11,4 +12,23 @@ import java.util.Optional; public interface PaymentRepository extends JpaRepository { Optional findByPaymentUuid(String paymentUuid); List findByUserId(Long userId); + + @Query(""" + SELECT COALESCE(SUM(p.tariff.tokens), 0) + FROM Payment p + WHERE p.operationType = 'TOKEN' + AND p.status = 'SUCCEEDED' + """) + Long getTotalTokensBought(); + + @Query(""" + SELECT + p.user.Id, + COALESCE(SUM(p.tariff.tokens), 0) + FROM Payment p + WHERE p.operationType = 'TOKEN' + AND p.status = 'SUCCEEDED' + GROUP BY p.user.Id + """) + List getTokensBoughtPerUser(); } diff --git a/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java index 0a39c38..8319718 100644 --- a/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TokenOperationRepository.java @@ -38,4 +38,19 @@ public interface TokenOperationRepository extends JpaRepository getTokensSpentPerUser(); } diff --git a/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java b/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java new file mode 100644 index 0000000..471127f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/statistic/TokenStatisticService.java @@ -0,0 +1,65 @@ +package ru.soune.nocopy.service.statistic; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import ru.soune.nocopy.dto.statistic.TokenStatisticResponse; +import ru.soune.nocopy.repository.PaymentRepository; +import ru.soune.nocopy.repository.TokenOperationRepository; +import ru.soune.nocopy.util.PercentileCalculator; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class TokenStatisticService { + private final PaymentRepository paymentRepository; + + private final TokenOperationRepository tokenOperationRepository; + + private static final int PERCENTILE_5 = 5; + + private static final int PERCENTILE_50 = 50; + + private static final int PERCENTILE_95 = 95; + + public TokenStatisticResponse getTokenStatistics() { + Long totalBought = paymentRepository.getTotalTokensBought(); + Long totalSpent = tokenOperationRepository.getTotalTokensSpent(); + + List boughtData = paymentRepository.getTokensBoughtPerUser(); + List boughtValues = extractTokenValues(boughtData); + + List spentData = tokenOperationRepository.getTokensSpentPerUser(); + List spentValues = extractTokenValues(spentData); + + PercentileCalculator.PercentilesResult boughtPercentiles = + PercentileCalculator.calculatePercentiles(boughtValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + PercentileCalculator.PercentilesResult spentPercentiles = + PercentileCalculator.calculatePercentiles(spentValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95); + + return TokenStatisticResponse.builder() + .totalBought(totalBought != null ? totalBought : 0L) + .totalSpent(totalSpent != null ? totalSpent : 0L) + .boughtPerUser(new TokenStatisticResponse.Percentiles( + boughtPercentiles.getP5(), + boughtPercentiles.getP50(), + boughtPercentiles.getP95() + )) + .spentPerUser(new TokenStatisticResponse.Percentiles( + spentPercentiles.getP5(), + spentPercentiles.getP50(), + spentPercentiles.getP95() + )) + .build(); + } + + private List extractTokenValues(List data) { + return data.stream() + .map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L) + .filter(value -> value > 0) + .collect(Collectors.toList()); + } +}