FundraiserService.java

package org.petify.funding.service;

import org.petify.funding.dto.FundraiserRequest;
import org.petify.funding.dto.FundraiserResponse;
import org.petify.funding.dto.FundraiserStats;
import org.petify.funding.exception.ResourceNotFoundException;
import org.petify.funding.model.Fundraiser;
import org.petify.funding.model.FundraiserStatus;
import org.petify.funding.repository.DonationRepository;
import org.petify.funding.repository.FundraiserRepository;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

@Service
@RequiredArgsConstructor
@Slf4j
public class FundraiserService {

    private final FundraiserRepository fundraiserRepository;
    private final DonationRepository donationRepository;

    @Transactional
    public FundraiserResponse create(FundraiserRequest request, Jwt jwt) {
        log.info("Creating fundraiser for shelter: {}", request.getShelterId());

        if (request.getIsMain() && fundraiserRepository.existsByShelterIdAndIsMainTrue(request.getShelterId())) {
            throw new IllegalStateException("Shelter already has a main fundraiser");
        }

        String username = jwt.getSubject();

        Fundraiser fundraiser = Fundraiser.builder()
                .shelterId(request.getShelterId())
                .title(request.getTitle())
                .description(request.getDescription())
                .goalAmount(request.getGoalAmount())
                .currency(request.getCurrency())
                .type(request.getType())
                .endDate(request.getEndDate())
                .isMain(request.getIsMain())
                .needs(request.getNeeds())
                .createdBy(username)
                .build();

        fundraiser = fundraiserRepository.save(fundraiser);
        return mapToResponse(fundraiser);
    }

    public FundraiserResponse get(Long id) {
        Fundraiser fundraiser = fundraiserRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Fundraiser not found with id: " + id));
        return mapToResponse(fundraiser);
    }

    public Page<FundraiserResponse> getByShelter(Long shelterId, Pageable pageable) {
        return fundraiserRepository.findByShelterId(shelterId, pageable)
                .map(this::mapToResponse);
    }

    public Page<FundraiserResponse> getActiveFundraisers(Pageable pageable) {
        return fundraiserRepository.findActiveByStatus(FundraiserStatus.ACTIVE, pageable)
                .map(this::mapToResponse);
    }

    public Optional<FundraiserResponse> getMainFundraiser(Long shelterId) {
        return fundraiserRepository.findByShelterIdAndIsMainTrue(shelterId)
                .map(this::mapToResponse);
    }

    @Transactional
    public FundraiserResponse update(Long id, FundraiserRequest request, Jwt jwt) {
        Fundraiser fundraiser = fundraiserRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Fundraiser not found with id: " + id));

        if (request.getIsMain() && !fundraiser.getIsMain()
                && fundraiserRepository.existsByShelterIdAndIsMainTrue(request.getShelterId())) {
            throw new IllegalStateException("Shelter already has a main fundraiser");
        }

        fundraiser.setTitle(request.getTitle());
        fundraiser.setDescription(request.getDescription());
        fundraiser.setGoalAmount(request.getGoalAmount());
        fundraiser.setCurrency(request.getCurrency());
        fundraiser.setType(request.getType());
        fundraiser.setEndDate(request.getEndDate());
        fundraiser.setIsMain(request.getIsMain());
        fundraiser.setNeeds(request.getNeeds());

        fundraiser = fundraiserRepository.save(fundraiser);
        return mapToResponse(fundraiser);
    }

    @Transactional
    public FundraiserResponse updateStatus(Long id, FundraiserStatus status) {
        Fundraiser fundraiser = fundraiserRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Fundraiser not found with id: " + id));

        fundraiser.setStatus(status);
        fundraiser = fundraiserRepository.save(fundraiser);
        return mapToResponse(fundraiser);
    }

    public FundraiserStats getStats(Long id) {
        Fundraiser fundraiser = fundraiserRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Fundraiser not found with id: " + id));

        BigDecimal currentAmount = donationRepository.sumCompletedDonationsByFundraiserId(id);
        if (currentAmount == null) {
            currentAmount = BigDecimal.ZERO;
        }

        Long totalDonations = donationRepository.countCompletedDonationsByFundraiserId(id);
        Long uniqueDonors = donationRepository.countUniqueDonorsByFundraiserId(id);

        Instant oneWeekAgo = Instant.now().minus(7, ChronoUnit.DAYS);
        BigDecimal lastWeekAmount = donationRepository.sumCompletedDonationsByFundraiserIdAndDateAfter(id, oneWeekAgo);
        if (lastWeekAmount == null) {
            lastWeekAmount = BigDecimal.ZERO;
        }

        BigDecimal averageDonation = totalDonations > 0
                ? currentAmount.divide(BigDecimal.valueOf(totalDonations), 2, RoundingMode.HALF_UP) :
                BigDecimal.ZERO;

        double progressPercentage = fundraiser.calculateProgress(currentAmount);
        BigDecimal remainingAmount = fundraiser.getGoalAmount().subtract(currentAmount);

        return FundraiserStats.builder()
                .fundraiserId(id)
                .title(fundraiser.getTitle())
                .goalAmount(fundraiser.getGoalAmount())
                .currentAmount(currentAmount)
                .remainingAmount(remainingAmount.max(BigDecimal.ZERO))
                .progressPercentage(progressPercentage)
                .totalDonations(totalDonations)
                .uniqueDonors(uniqueDonors)
                .averageDonation(averageDonation)
                .lastWeekAmount(lastWeekAmount)
                .isGoalReached(currentAmount.compareTo(fundraiser.getGoalAmount()) >= 0)
                .build();
    }

    @Transactional
    public void delete(Long id) {
        Fundraiser fundraiser = fundraiserRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Fundraiser not found with id: " + id));

        Long donationCount = donationRepository.countByFundraiserId(id);
        if (donationCount > 0) {
            throw new IllegalStateException("Cannot delete fundraiser with existing donations");
        }

        fundraiserRepository.delete(fundraiser);
    }

    private FundraiserResponse mapToResponse(Fundraiser fundraiser) {
        BigDecimal currentAmount = donationRepository.sumCompletedDonationsByFundraiserId(fundraiser.getId());
        if (currentAmount == null) {
            currentAmount = BigDecimal.ZERO;
        }

        Long donationCount = donationRepository.countCompletedDonationsByFundraiserId(fundraiser.getId());
        double progressPercentage = fundraiser.calculateProgress(currentAmount);

        return FundraiserResponse.builder()
                .id(fundraiser.getId())
                .shelterId(fundraiser.getShelterId())
                .title(fundraiser.getTitle())
                .description(fundraiser.getDescription())
                .goalAmount(fundraiser.getGoalAmount())
                .currency(fundraiser.getCurrency())
                .status(fundraiser.getStatus())
                .type(fundraiser.getType())
                .startDate(fundraiser.getStartDate())
                .endDate(fundraiser.getEndDate())
                .isMain(fundraiser.getIsMain())
                .needs(fundraiser.getNeeds())
                .createdBy(fundraiser.getCreatedBy())
                .createdAt(fundraiser.getCreatedAt())
                .updatedAt(fundraiser.getUpdatedAt())
                .completedAt(fundraiser.getCompletedAt())
                .cancelledAt(fundraiser.getCancelledAt())
                .currentAmount(currentAmount)
                .donationCount(donationCount)
                .progressPercentage(progressPercentage)
                .canAcceptDonations(fundraiser.canAcceptDonations())
                .build();
    }
}