PaymentAnalyticsService.java
package org.petify.funding.service;
import org.petify.funding.dto.PaymentAnalyticsResponse;
import org.petify.funding.model.PaymentAnalytics;
import org.petify.funding.model.PaymentProvider;
import org.petify.funding.repository.PaymentAnalyticsRepository;
import org.petify.funding.repository.PaymentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentAnalyticsService {
private final PaymentAnalyticsRepository analyticsRepository;
private final PaymentRepository paymentRepository;
public List<PaymentAnalyticsResponse> getAnalytics(LocalDate startDate, LocalDate endDate, String provider) {
List<PaymentAnalytics> analytics;
if (provider != null && !provider.isEmpty()) {
PaymentProvider paymentProvider = PaymentProvider.valueOf(provider.toUpperCase());
analytics = analyticsRepository.findByDateBetweenAndProvider(startDate, endDate, paymentProvider);
} else {
analytics = analyticsRepository.findByDateBetween(startDate, endDate);
}
return analytics.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
public Map<String, Object> getPaymentStatsSummary(int days) {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(days);
Instant startInstant = startDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
Instant endInstant = endDate.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant();
Map<String, Object> stats = new HashMap<>();
List<Object[]> overallStats = paymentRepository.getPaymentStatistics(startInstant, endInstant);
if (!overallStats.isEmpty()) {
Object[] row = overallStats.get(0);
stats.put("totalTransactions", row[0]);
stats.put("successfulTransactions", row[1]);
stats.put("failedTransactions", row[2]);
stats.put("totalAmount", row[3]);
stats.put("totalFees", row[4]);
stats.put("successRate", calculateSuccessRate((Long) row[1], (Long) row[0]));
}
Map<String, Object> providerStats = new HashMap<>();
for (PaymentProvider provider : PaymentProvider.values()) {
List<Object[]> providerData = paymentRepository.getPaymentStatisticsByProvider(
startInstant, endInstant, provider);
if (!providerData.isEmpty()) {
Object[] row = providerData.get(0);
Map<String, Object> providerInfo = new HashMap<>();
providerInfo.put("totalTransactions", row[0]);
providerInfo.put("successfulTransactions", row[1]);
providerInfo.put("totalAmount", row[2]);
providerInfo.put("successRate", calculateSuccessRate((Long) row[1], (Long) row[0]));
providerStats.put(provider.name().toLowerCase(), providerInfo);
}
}
stats.put("providerBreakdown", providerStats);
List<Map<String, Object>> dailyTrends = new ArrayList<>();
for (int i = days - 1; i >= 0; i--) {
LocalDate date = endDate.minusDays(i);
Instant dayStart = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
Instant dayEnd = date.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant();
List<Object[]> dailyStats = paymentRepository.getPaymentStatistics(dayStart, dayEnd);
Map<String, Object> dayStats = new HashMap<>();
dayStats.put("date", date);
if (!dailyStats.isEmpty()) {
Object[] row = dailyStats.get(0);
dayStats.put("transactions", row[0]);
dayStats.put("amount", row[3]);
dayStats.put("successRate", calculateSuccessRate((Long) row[1], (Long) row[0]));
} else {
dayStats.put("transactions", 0);
dayStats.put("amount", BigDecimal.ZERO);
dayStats.put("successRate", BigDecimal.ZERO);
}
dailyTrends.add(dayStats);
}
stats.put("dailyTrends", dailyTrends);
Map<String, Object> currencyStats = paymentRepository.getPaymentStatsByCurrency(startInstant, endInstant);
stats.put("currencyBreakdown", currencyStats);
Map<String, Object> methodStats = paymentRepository.getPaymentStatsByMethod(startInstant, endInstant);
stats.put("methodBreakdown", methodStats);
return stats;
}
@Scheduled(cron = "0 0 1 * * ?")
@Transactional
public void generateDailyAnalytics() {
LocalDate yesterday = LocalDate.now().minusDays(1);
log.info("Generating analytics for {}", yesterday);
for (PaymentProvider provider : PaymentProvider.values()) {
generateAnalyticsForDate(yesterday, provider);
}
log.info("Daily analytics generation completed");
}
@Transactional
public void generateAnalyticsForDate(LocalDate date, PaymentProvider provider) {
Optional<PaymentAnalytics> existing = analyticsRepository
.findByDateAndProvider(date, provider);
if (existing.isPresent()) {
log.debug("Analytics already exist for {} and {}", date, provider);
return;
}
Instant startOfDay = date.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
Instant endOfDay = date.atTime(LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant();
List<Object[]> stats = paymentRepository.getPaymentStatisticsByProvider(
startOfDay, endOfDay, provider);
if (stats.isEmpty()) {
log.debug("No payments found for {} and {}", date, provider);
return;
}
Object[] row = stats.get(0);
PaymentAnalytics analytics = PaymentAnalytics.builder()
.date(date)
.provider(provider)
.totalTransactions(((Long) row[0]).intValue())
.successfulTransactions(((Long) row[1]).intValue())
.failedTransactions(((Long) row[0]).intValue() - ((Long) row[1]).intValue())
.totalAmount((BigDecimal) row[2])
.totalFees(calculateTotalFees(startOfDay, endOfDay, provider))
.successRate(calculateSuccessRate((Long) row[1], (Long) row[0]))
.averageTransactionAmount(((BigDecimal) row[2]).divide(
new BigDecimal((Long) row[0]), 2, RoundingMode.HALF_UP))
.build();
analyticsRepository.save(analytics);
log.debug("Analytics saved for {} and {}", date, provider);
}
private BigDecimal calculateTotalFees(Instant startOfDay, Instant endOfDay,
PaymentProvider provider) {
List<BigDecimal> fees = paymentRepository.getFeeAmountsByDateRangeAndProvider(
startOfDay, endOfDay, provider);
return fees.stream()
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private PaymentAnalyticsResponse toResponse(PaymentAnalytics analytics) {
return PaymentAnalyticsResponse.builder()
.date(analytics.getDate())
.provider(analytics.getProvider())
.totalTransactions(analytics.getTotalTransactions())
.successfulTransactions(analytics.getSuccessfulTransactions())
.failedTransactions(analytics.getFailedTransactions())
.totalAmount(analytics.getTotalAmount())
.totalFees(analytics.getTotalFees())
.successRate(analytics.getSuccessRate())
.averageTransactionAmount(analytics.getAverageTransactionAmount())
.build();
}
private BigDecimal calculateSuccessRate(Long successful, Long total) {
if (total == null || total == 0) {
return BigDecimal.ZERO;
}
return new BigDecimal(successful)
.divide(new BigDecimal(total), 4, RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}
}