DonationController.java

package org.petify.funding.controller;

import org.petify.funding.dto.DonationIntentRequest;
import org.petify.funding.dto.DonationResponse;
import org.petify.funding.dto.DonationStatistics;
import org.petify.funding.dto.DonationWithPaymentStatusResponse;
import org.petify.funding.dto.PaymentChoiceRequest;
import org.petify.funding.dto.PaymentInitializationResponse;
import org.petify.funding.dto.PaymentOptionsResponse;
import org.petify.funding.dto.PaymentProviderOption;
import org.petify.funding.dto.PaymentResponse;
import org.petify.funding.model.DonationStatus;
import org.petify.funding.model.DonationType;
import org.petify.funding.service.DonationService;
import org.petify.funding.service.PaymentService;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.util.Base64;
import java.util.List;

@RestController
@RequestMapping("/donations")
@RequiredArgsConstructor
@Slf4j
public class DonationController {

    private final DonationService donationService;
    private final PaymentService paymentService;

    @PostMapping("/intent")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<PaymentOptionsResponse> createDonationIntent(
            @RequestBody @Valid DonationIntentRequest request,
            @AuthenticationPrincipal Jwt jwt) {

        log.info("Creating donation intent for user: {}", jwt.getSubject());

        DonationResponse donation = donationService.createDraft(request, jwt);

        List<PaymentProviderOption> options = paymentService.getAvailablePaymentOptions(
                donation.getAmount(), getCurrentUserLocation());

        String sessionToken = generateSessionToken(donation.getId());

        PaymentOptionsResponse response = PaymentOptionsResponse.builder()
                .donationId(donation.getId())
                .donation(donation)
                .availableProviders(options)
                .sessionToken(sessionToken)
                .build();

        return ResponseEntity.ok(response);
    }

    @PostMapping("/{donationId}/payment/initialize")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<PaymentInitializationResponse> initializePayment(
            @PathVariable Long donationId,
            @RequestBody @Valid PaymentChoiceRequest request,
            @RequestHeader("Session-Token") String sessionToken) {

        log.info("Initializing payment for donation {} with provider {}",
                donationId, request.getProvider());

        validateSessionToken(sessionToken, donationId);

        PaymentInitializationResponse response = paymentService.initializePayment(
                donationId, request);

        return ResponseEntity.ok(response);
    }

    @GetMapping("/payment-status/{donationId}")
    public ResponseEntity<DonationWithPaymentStatusResponse> checkPaymentStatus(
            @PathVariable Long donationId) {

        DonationResponse donation = donationService.get(donationId);

        List<PaymentResponse> payments = paymentService.getPaymentsByDonation(donationId);
        PaymentResponse latestPayment = payments.isEmpty() ? null :
                payments.stream()
                        .max((p1, p2) -> p1.getCreatedAt().compareTo(p2.getCreatedAt()))
                        .orElse(null);

        DonationWithPaymentStatusResponse response = DonationWithPaymentStatusResponse.builder()
                .donation(donation)
                .latestPayment(latestPayment)
                .isCompleted(donation.getStatus() == DonationStatus.COMPLETED)
                .build();

        return ResponseEntity.ok(response);
    }

    @PutMapping("/{donationId}/cancel")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<DonationResponse> cancelDonation(@PathVariable Long donationId) {
        DonationResponse donation = donationService.cancelDonation(donationId);
        return ResponseEntity.ok(donation);
    }

    @PostMapping("/{donationId}/refund")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public ResponseEntity<DonationResponse> refundDonation(
            @PathVariable Long donationId,
            @RequestParam(required = false) BigDecimal amount) {
        DonationResponse donation = donationService.refundDonation(donationId, amount);
        return ResponseEntity.ok(donation);
    }

    @GetMapping
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public ResponseEntity<Page<DonationResponse>> getAllDonations(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) DonationType type) {

        Page<DonationResponse> donations = donationService.getAll(
                PageRequest.of(page, size, Sort.by("createdAt").descending()), type);
        return ResponseEntity.ok(donations);
    }

    @GetMapping("/my")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<Page<DonationResponse>> getMyDonations(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Page<DonationResponse> donations = donationService.getUserDonations(
                PageRequest.of(page, size, Sort.by("createdAt").descending()));
        return ResponseEntity.ok(donations);
    }

    @GetMapping("/{id}")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<DonationResponse> getDonationById(@PathVariable Long id) {
        DonationResponse donation = donationService.get(id);
        return ResponseEntity.ok(donation);
    }

    @GetMapping("/shelter/{shelterId}")
    public ResponseEntity<Page<DonationResponse>> getDonationsByShelter(
            @PathVariable Long shelterId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Page<DonationResponse> donations = donationService.getForShelter(
                shelterId, PageRequest.of(page, size, Sort.by("donatedAt").descending()));
        return ResponseEntity.ok(donations);
    }

    @GetMapping("/pet/{petId}")
    public ResponseEntity<Page<DonationResponse>> getDonationsByPet(
            @PathVariable Long petId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Page<DonationResponse> donations = donationService.getForPet(
                petId, PageRequest.of(page, size, Sort.by("donatedAt").descending()));
        return ResponseEntity.ok(donations);
    }

    @GetMapping("/shelter/{shelterId}/stats")
    public ResponseEntity<DonationStatistics> getShelterStats(@PathVariable Long shelterId) {
        DonationStatistics stats = donationService.getShelterDonationStats(shelterId);
        return ResponseEntity.ok(stats);
    }

    @GetMapping("/fundraiser/{fundraiserId}")
    public ResponseEntity<Page<DonationResponse>> getDonationsByFundraiser(
            @PathVariable Long fundraiserId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {

        Page<DonationResponse> donations = donationService.getForFundraiser(
                fundraiserId, PageRequest.of(page, size, Sort.by("donatedAt").descending()));
        return ResponseEntity.ok(donations);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public void deleteDonation(@PathVariable Long id) {
        donationService.delete(id);
    }

    private String getCurrentUserLocation() {
        return "PL";
    }

    private String generateSessionToken(Long donationId) {
        return Base64.getEncoder().encodeToString(
                (donationId + ":" + System.currentTimeMillis()).getBytes());
    }

    private void validateSessionToken(String sessionToken, Long donationId) {
        try {
            String decoded = new String(Base64.getDecoder().decode(sessionToken));
            String expectedPrefix = donationId + ":";

            if (!decoded.startsWith(expectedPrefix)) {
                throw new RuntimeException("Invalid session token");
            }

            long timestamp = Long.parseLong(decoded.substring(expectedPrefix.length()));
            long now = System.currentTimeMillis();

            if (now - timestamp > 30 * 60 * 1000) {
                throw new RuntimeException("Session token expired");
            }

        } catch (Exception e) {
            throw new RuntimeException("Invalid session token", e);
        }
    }
}