From 1a9dc36cf5248129e8247c7fd070897f25bf1010 Mon Sep 17 00:00:00 2001 From: Miroslav Date: Sat, 30 Dec 2023 21:44:07 +0200 Subject: [PATCH] Add statistics endpoint for fetching total spending by category for time period --- .../controllers/StatisticsController.java | 41 ++++++++++++++- .../finances/dtos/SpendingByCategoryDTO.java | 11 ++++ .../dtos/SpendingOverTimeByCategoryDTO.java | 11 ++++ .../mvvasilev/finances/enums/TimePeriod.java | 13 +++++ .../persistence/StatisticsRepository.java | 50 +++++++++++++++++++ .../finances/services/StatisticsService.java | 37 ++++++++++++++ 6 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingByCategoryDTO.java create mode 100644 PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingOverTimeByCategoryDTO.java create mode 100644 PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TimePeriod.java create mode 100644 PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java create mode 100644 PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java index 6732537..f4d2072 100644 --- a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/controllers/StatisticsController.java @@ -1,10 +1,49 @@ package dev.mvvasilev.finances.controllers; +import dev.mvvasilev.common.controller.AbstractRestController; +import dev.mvvasilev.common.web.APIResponseDTO; +import dev.mvvasilev.finances.dtos.SpendingByCategoryDTO; +import dev.mvvasilev.finances.dtos.SpendingOverTimeByCategoryDTO; +import dev.mvvasilev.finances.enums.TimePeriod; +import dev.mvvasilev.finances.services.StatisticsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.time.LocalDateTime; +import java.util.Collection; + @RestController("/statistics") -public class StatisticsController { +public class StatisticsController extends AbstractRestController { + private final StatisticsService statisticsService; + @Autowired + public StatisticsController(StatisticsService statisticsService) { + this.statisticsService = statisticsService; + } + + @GetMapping("/totalSpendingByCategory") + public ResponseEntity> fetchSpendingByCategory( + @RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, + @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, + Authentication authentication + ) { + return ok(statisticsService.spendingByCategory(from, to, Integer.parseInt(authentication.getName()))); + } + + @GetMapping("/spendingOverTimeByCategory") + public ResponseEntity> fetchSpendingOverTimeByCategory( + Collection categoryId, + TimePeriod period, + @RequestParam(defaultValue = "1970-01-01T00:00:00") LocalDateTime from, + @RequestParam(defaultValue = "2099-01-01T00:00:00") LocalDateTime to, + Authentication authentication + ) { + throw new UnsupportedOperationException(); + } } diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingByCategoryDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingByCategoryDTO.java new file mode 100644 index 0000000..1acfe24 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingByCategoryDTO.java @@ -0,0 +1,11 @@ +package dev.mvvasilev.finances.dtos; + +import java.util.Collection; +import java.util.Map; + +public record SpendingByCategoryDTO( + Collection categories, + + Map spendingByCategory +) { +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingOverTimeByCategoryDTO.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingOverTimeByCategoryDTO.java new file mode 100644 index 0000000..e1f1986 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/dtos/SpendingOverTimeByCategoryDTO.java @@ -0,0 +1,11 @@ +package dev.mvvasilev.finances.dtos; + +import java.util.Collection; +import java.util.Map; + +public record SpendingOverTimeByCategoryDTO( + Collection categories, + + Map> spendingOverTime +) { +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TimePeriod.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TimePeriod.java new file mode 100644 index 0000000..9932a85 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/enums/TimePeriod.java @@ -0,0 +1,13 @@ +package dev.mvvasilev.finances.enums; + +public enum TimePeriod { + SECONDLY, + MINUTELY, + HOURLY, + DAILY, + WEEKLY, + BIWEEKLY, + MONTHLY, + QUARTERLY, + YEARLY +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java new file mode 100644 index 0000000..ff68bbc --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/persistence/StatisticsRepository.java @@ -0,0 +1,50 @@ +package dev.mvvasilev.finances.persistence; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import jakarta.persistence.Tuple; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Repository +public class StatisticsRepository { + + private final EntityManager entityManager; + + @Autowired + public StatisticsRepository(EntityManager entityManager) { + this.entityManager = entityManager; + } + + public Map fetchSpendingByCategory(LocalDateTime from, LocalDateTime to, List categoryIds) { + Query nativeQuery = entityManager.createNativeQuery( + """ + SELECT ptc.category_id AS category_id, ROUND(CAST(SUM(pt.amount) AS NUMERIC), 2) AS total_spending + FROM transactions.processed_transaction AS pt + JOIN categories.processed_transaction_category AS ptc ON ptc.processed_transaction_id = pt.id + WHERE + pt.is_inflow = FALSE + AND ptc.category_id IN (?1) + AND (pt.timestamp BETWEEN ?2 AND ?3) + GROUP BY ptc.category_id + ORDER BY total_spending DESC; + """, + Tuple.class + ); + + nativeQuery.setParameter(1, categoryIds); + nativeQuery.setParameter(2, from); + nativeQuery.setParameter(3, to); + + //noinspection unchecked + return (Map) nativeQuery.getResultStream().collect(Collectors.toMap( + (Tuple tuple) -> ((Number) tuple.get("category_id")).longValue(), + (Tuple tuple) -> ((Number) tuple.get("total_spending")).doubleValue() + )); + } +} diff --git a/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java new file mode 100644 index 0000000..c9ab059 --- /dev/null +++ b/PersonalFinancesService/src/main/java/dev/mvvasilev/finances/services/StatisticsService.java @@ -0,0 +1,37 @@ +package dev.mvvasilev.finances.services; + +import dev.mvvasilev.common.data.AbstractEntity; +import dev.mvvasilev.finances.dtos.CategoryDTO; +import dev.mvvasilev.finances.dtos.SpendingByCategoryDTO; +import dev.mvvasilev.finances.persistence.StatisticsRepository; +import dev.mvvasilev.finances.persistence.TransactionCategoryRepository; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class StatisticsService { + + private final TransactionCategoryRepository transactionCategoryRepository; + + private final StatisticsRepository statisticsRepository; + + public StatisticsService(TransactionCategoryRepository transactionCategoryRepository, StatisticsRepository statisticsRepository) { + this.transactionCategoryRepository = transactionCategoryRepository; + this.statisticsRepository = statisticsRepository; + } + + public SpendingByCategoryDTO spendingByCategory(LocalDateTime from, LocalDateTime to, int userId) { + final var categories = transactionCategoryRepository.fetchTransactionCategoriesWithUserId(userId); + final var spendingByCategory = statisticsRepository.fetchSpendingByCategory( + from, + to, + categories.stream().map(AbstractEntity::getId).toList() + ); + + return new SpendingByCategoryDTO( + categories.stream().map(c -> new CategoryDTO(c.getId(), c.getName(), null)).toList(), + spendingByCategory + ); + } +}