Add statistics endpoint for fetching total spending by category for time period

This commit is contained in:
Miroslav Vasilev 2023-12-30 21:44:07 +02:00
parent acfbe7033a
commit 1a9dc36cf5
6 changed files with 162 additions and 1 deletions

View file

@ -1,10 +1,49 @@
package dev.mvvasilev.finances.controllers; 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 org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.Collection;
@RestController("/statistics") @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<APIResponseDTO<SpendingByCategoryDTO>> 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<APIResponseDTO<SpendingOverTimeByCategoryDTO>> fetchSpendingOverTimeByCategory(
Collection<Long> 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();
}
} }

View file

@ -0,0 +1,11 @@
package dev.mvvasilev.finances.dtos;
import java.util.Collection;
import java.util.Map;
public record SpendingByCategoryDTO(
Collection<CategoryDTO> categories,
Map<Long, Double> spendingByCategory
) {
}

View file

@ -0,0 +1,11 @@
package dev.mvvasilev.finances.dtos;
import java.util.Collection;
import java.util.Map;
public record SpendingOverTimeByCategoryDTO(
Collection<CategoryDTO> categories,
Map<Long, Collection<Double>> spendingOverTime
) {
}

View file

@ -0,0 +1,13 @@
package dev.mvvasilev.finances.enums;
public enum TimePeriod {
SECONDLY,
MINUTELY,
HOURLY,
DAILY,
WEEKLY,
BIWEEKLY,
MONTHLY,
QUARTERLY,
YEARLY
}

View file

@ -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<Long, Double> fetchSpendingByCategory(LocalDateTime from, LocalDateTime to, List<Long> 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<Long, Double>) nativeQuery.getResultStream().collect(Collectors.toMap(
(Tuple tuple) -> ((Number) tuple.get("category_id")).longValue(),
(Tuple tuple) -> ((Number) tuple.get("total_spending")).doubleValue()
));
}
}

View file

@ -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
);
}
}