package com.parkingexchange.reward.application.dto;


import java.time.LocalDateTime;
import java.util.UUID;

public class AvailabilityRequest {

    private UUID leavingUserId;
    private double latitude;
    private double longitude;
    private LocalDateTime estimatedLeavingTime;


    // Getters and Setters

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    public void setLeavingUserId(UUID leavingUserId) {
        this.leavingUserId = leavingUserId;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public LocalDateTime getEstimatedLeavingTime() {
        return estimatedLeavingTime;
    }

    public void setEstimatedLeavingTime(LocalDateTime estimatedLeavingTime) {
        this.estimatedLeavingTime = estimatedLeavingTime;
    }

}package com.parkingexchange.reward.application.dto;


import com.parkingexchange.reward.domain.model.AvailabilityStatus;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

public class AvailabilityResponse {
    private UUID availabilityId;
    private UUID leavingUserId;
    private double latitude;
    private double longitude;
    private LocalDateTime estimatedLeavingTime;
    private AvailabilityStatus status;
    private UUID reservedByUserId;
    private List<ReservationRequestResponse> reservationRequests;

    // Getters and setters


    // Getters and Setters

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public void setAvailabilityId(UUID availabilityId) {
        this.availabilityId = availabilityId;
    }

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    public void setLeavingUserId(UUID leavingUserId) {
        this.leavingUserId = leavingUserId;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public LocalDateTime getEstimatedLeavingTime() {
        return estimatedLeavingTime;
    }

    public void setEstimatedLeavingTime(LocalDateTime estimatedLeavingTime) {
        this.estimatedLeavingTime = estimatedLeavingTime;
    }

    public AvailabilityStatus getStatus() {
        return status;
    }

    public void setStatus(AvailabilityStatus status) {
        this.status = status;
    }

    public UUID getReservedByUserId() {
        return reservedByUserId;
    }

    public void setReservedByUserId(UUID reservedByUserId) {
        this.reservedByUserId = reservedByUserId;
    }

    public List<ReservationRequestResponse> getReservationRequests() {
        return reservationRequests;
    }

    public void setReservationRequests(List<ReservationRequestResponse> reservationRequests) {
        this.reservationRequests = reservationRequests;
    }
}package com.parkingexchange.reward.application.dto;

import java.util.UUID;

public class CancelAvailabilityRequest {

    private UUID userId;

    // Getters and Setters

    public UUID getUserId() {
        return userId;
    }

    public void setUserId(UUID userId) {
        this.userId = userId;
    }
}
package com.parkingexchange.reward.application.dto;

import java.util.UUID;

public class ConfirmSpotTakenRequest {

    private UUID arrivingUserId;

    // Getters and Setters

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

    public void setArrivingUserId(UUID arrivingUserId) {
        this.arrivingUserId = arrivingUserId;
    }
}
package com.parkingexchange.reward.application.dto.mapper;

import com.parkingexchange.reward.application.dto.AvailabilityResponse;
import com.parkingexchange.reward.application.dto.ReservationRequestResponse;
import com.parkingexchange.reward.domain.model.Availability;
import com.parkingexchange.reward.domain.model.AvailabilityId;
import com.parkingexchange.reward.domain.model.ReservationRequest;
import org.mapstruct.Mapper;

import java.util.UUID;

@Mapper(componentModel = "spring")
public interface AvailabilityMapper {

    AvailabilityResponse toResponse(Availability availability);

    ReservationRequestResponse toReservationResponse(ReservationRequest request);

    default UUID map (AvailabilityId value){
        return value.getValue();
    }
    // Add other mapping methods if needed
}
package com.parkingexchange.reward.application.dto;

import java.util.UUID;

public class ReservationRequestDto {

    private UUID arrivingUserId;
    private String message;

    // Getters and Setters

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

    public void setArrivingUserId(UUID arrivingUserId) {
        this.arrivingUserId = arrivingUserId;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
package com.parkingexchange.reward.application.dto;

import java.util.UUID;


import com.parkingexchange.reward.domain.model.RequestStatus;
import java.time.LocalDateTime;

public class ReservationRequestResponse {

    private UUID requestId;
    private UUID arrivingUserId;
    private UUID leavingUserId;
    private RequestStatus status;
    private LocalDateTime requestedAt;
    private LocalDateTime respondedAt;
    private String message;

    // Getters and Setters

    public UUID getRequestId() {
        return requestId;
    }

    public void setRequestId(UUID requestId) {
        this.requestId = requestId;
    }

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

    public void setArrivingUserId(UUID arrivingUserId) {
        this.arrivingUserId = arrivingUserId;
    }

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    public void setLeavingUserId(UUID leavingUserId) {
        this.leavingUserId = leavingUserId;
    }

    public RequestStatus getStatus() {
        return status;
    }

    public void setStatus(RequestStatus status) {
        this.status = status;
    }

    public LocalDateTime getRequestedAt() {
        return requestedAt;
    }

    public void setRequestedAt(LocalDateTime requestedAt) {
        this.requestedAt = requestedAt;
    }

    public LocalDateTime getRespondedAt() {
        return respondedAt;
    }

    public void setRespondedAt(LocalDateTime respondedAt) {
        this.respondedAt = respondedAt;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}package com.parkingexchange.reward.application.dto;

import com.parkingexchange.reward.domain.model.RequestStatus;

import java.time.LocalDateTime;
import java.util.UUID;

public class ReservationResponseDto {

    private UUID leavingUserId;
    private boolean accepted;

    // Getters and Setters

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    public void setLeavingUserId(UUID leavingUserId) {
        this.leavingUserId = leavingUserId;
    }

    public boolean isAccepted() {
        return accepted;
    }

    public void setAccepted(boolean accepted) {
        this.accepted = accepted;
    }
}package com.parkingexchange.reward.application.dto;

public class SearchCriteria {

    private double latitude;
    private double longitude;
    private double radius;

    // Getters and Setters

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}package com.parkingexchange.reward.application.service;



import com.parkingexchange.common.domain.model.DomainEvent;
import com.parkingexchange.common.domain.model.DomainEventPublisher;
import com.parkingexchange.reward.application.dto.*;
import com.parkingexchange.reward.application.dto.mapper.AvailabilityMapper;
import com.parkingexchange.reward.domain.model.*;
import com.parkingexchange.reward.domain.repository.AvailabilityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;


@Service
public class AvailabilityApplicationServiceImpl implements AvailabilityApplicationService {

    private final AvailabilityRepository availabilityRepository;
    private final AvailabilityMapper availabilityMapper;
    private final DomainEventPublisher domainEventPublisher;

    @Autowired
    public AvailabilityApplicationServiceImpl(AvailabilityRepository availabilityRepository,
                                              AvailabilityMapper availabilityMapper,
                                              DomainEventPublisher domainEventPublisher) {
        this.availabilityRepository = availabilityRepository;
        this.availabilityMapper = availabilityMapper;
        this.domainEventPublisher = domainEventPublisher;
    }

    @Transactional
    @Override
    public AvailabilityResponse reportAvailability(AvailabilityRequest request) {
        Location location = new Location(request.getLatitude(), request.getLongitude());
        Availability availability = Availability.create(
                request.getLeavingUserId(),
                location,
                request.getEstimatedLeavingTime()
        );

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

    @Override
    public List<AvailabilityResponse> searchAvailableSpots(SearchCriteria criteria) {
        List<Availability> availabilities = availabilityRepository.findAvailableInArea(
                criteria.getLatitude(),
                criteria.getLongitude(),
                criteria.getRadius()
        );

        return availabilities.stream()
                .map(availabilityMapper::toResponse)
                .collect(Collectors.toList());
    }

    @Transactional
    @Override
    public ReservationRequestResponse requestReservation(AvailabilityId availabilityId, ReservationRequestDto requestDto) {

        Availability availability = availabilityRepository.findByAvailabilityId(availabilityId)
                .orElseThrow(() -> new IllegalArgumentException("Availability not found"));

        // Delegate to the aggregate root
        ReservationRequest reservationRequest = availability.createReservationRequest(
                requestDto.getArrivingUserId(),
                requestDto.getMessage()
        );

        // Save the aggregate
        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toReservationResponse(reservationRequest);

    }

    @Transactional
    @Override
    public ReservationRequestResponse respondToReservation(UUID requestId, ReservationResponseDto responseDto) {
        Availability availability = availabilityRepository.findByReservationRequestId(requestId)
                .orElseThrow(() -> new IllegalArgumentException("Availability not found"));

        // Delegate to the aggregate root
        availability.respondToReservationRequest(requestId, responseDto.getLeavingUserId(), responseDto.isAccepted());

        // Save the aggregate
        availabilityRepository.save(availability);

        // Retrieve the updated reservation request
        ReservationRequest reservationRequest = availability.getReservationRequests().stream()
                .filter(r -> r.getRequestId().equals(requestId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Reservation request not found"));

        publishDomainEvents(availability);

        return availabilityMapper.toReservationResponse(reservationRequest);
    }

    @Transactional
    @Override
    public AvailabilityResponse confirmSpotTaken(AvailabilityId availabilityId, ConfirmSpotTakenRequest request) {
        Availability availability = availabilityRepository.findByAvailabilityId(availabilityId)
                .orElseThrow(() -> new IllegalArgumentException("Availability not found"));

        availability.confirmTaken(request.getArrivingUserId());

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

    @Transactional
    @Override
    public AvailabilityResponse cancelAvailability(AvailabilityId availabilityId, CancelAvailabilityRequest request) {
        Availability availability = availabilityRepository.findByAvailabilityId(availabilityId)
                .orElseThrow(() -> new IllegalArgumentException("Availability not found"));

        availability.cancel(request.getUserId());

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

    private void publishDomainEvents(Availability availability) {
        List<DomainEvent> events = availability.domainEvents();
        for (DomainEvent event : events) {
            domainEventPublisher.publish(event);
        }
    }
}package com.parkingexchange.reward.application.service;


import com.parkingexchange.reward.application.dto.*;
import com.parkingexchange.reward.domain.model.AvailabilityId;

import java.util.List;
import java.util.UUID;

public interface AvailabilityApplicationService {

    AvailabilityResponse reportAvailability(AvailabilityRequest request);

    List<AvailabilityResponse> searchAvailableSpots(SearchCriteria criteria);

    ReservationRequestResponse requestReservation(AvailabilityId availabilityId, ReservationRequestDto requestDto);

    ReservationRequestResponse respondToReservation(UUID requestId, ReservationResponseDto responseDto);

    AvailabilityResponse confirmSpotTaken(AvailabilityId availabilityId, ConfirmSpotTakenRequest request);

    AvailabilityResponse cancelAvailability(AvailabilityId availabilityId, CancelAvailabilityRequest request);
}package com.parkingexchange.reward.domain.event;

import com.parkingexchange.common.domain.model.DomainEvent;

import java.time.Instant;
import java.util.UUID;

public class AvailabilityCanceledEvent implements DomainEvent {

    private final UUID availabilityId;
    private final UUID arrivingUserId;
    private final Instant occurredOn;
    private final String moduleName = "parking"; // TODO you should't set this manually


    public AvailabilityCanceledEvent(UUID availabilityId, UUID arrivingUserId) {
        this.occurredOn = Instant.now();
        this.availabilityId = availabilityId;
        this.arrivingUserId = arrivingUserId;
    }

    @Override
    public int eventVersion() {
        return 1;
    }

    @Override
    public Instant occurredOn() {
        return occurredOn;
    }

    @Override
    public String moduleName() {
        return moduleName;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

}
package com.parkingexchange.reward.domain.event;

import com.parkingexchange.common.domain.model.DomainEvent;

import java.time.Instant;
import java.util.UUID;

public class ReservationRequestCreatedEvent implements DomainEvent {

    private final UUID availabilityId;
    private final UUID requestId;
    private final UUID arrivingUserId;
    private final String message;
    private final Instant occurredOn;
    private final String moduleName = "parking"; // TODO you should't set this manually

    public ReservationRequestCreatedEvent(UUID availabilityId, UUID requestId, UUID arrivingUserId, String message) {
        this.availabilityId = availabilityId;
        this.requestId = requestId;
        this.arrivingUserId = arrivingUserId;
        this.message = message;
        this.occurredOn = Instant.now();
    }

    @Override
    public int eventVersion() {
        return 1;
    }

    @Override
    public Instant occurredOn() {
        return occurredOn;
    }

    @Override
    public String moduleName() {
        return moduleName;
    }

}

package com.parkingexchange.reward.domain.event;

import com.parkingexchange.common.domain.model.DomainEvent;

import java.time.Instant;
import java.util.UUID;

public class ReservationRequestRespondedEvent implements DomainEvent {

    private final UUID availabilityId;
    private final UUID requestId;
    private final boolean accepted;
    private final Instant occurredOn;
    private final String moduleName = "parking"; // TODO you should't set this manually


    public ReservationRequestRespondedEvent(UUID availabilityId, UUID requestId, boolean accepted) {
        this.occurredOn = Instant.now();
        this.availabilityId = availabilityId;
        this.requestId = requestId;
        this.accepted = accepted;
    }

    @Override
    public int eventVersion() {
        return 1;
    }

    @Override
    public Instant occurredOn() {
        return occurredOn;
    }

    @Override
    public String moduleName() {
        return moduleName;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getRequestId() {
        return requestId;
    }

    public boolean isAccepted() {
        return accepted;
    }
}
package com.parkingexchange.reward.domain.event;

import com.parkingexchange.common.domain.model.DomainEvent;

import java.time.Instant;
import java.util.UUID;

public class SpotTakenConfirmedEvent implements DomainEvent {

    private final UUID availabilityId;
    private final UUID arrivingUserId;
    private final Instant occurredOn;
    private final String moduleName = "parking"; // TODO you should't set this manually


    public SpotTakenConfirmedEvent(UUID availabilityId, UUID arrivingUserId) {
        this.occurredOn = Instant.now();
        this.availabilityId = availabilityId;
        this.arrivingUserId = arrivingUserId;
    }

    @Override
    public int eventVersion() {
        return 1;
    }

    @Override
    public Instant occurredOn() {
        return occurredOn;
    }

    @Override
    public String moduleName() {
        return moduleName;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

}
package com.parkingexchange.reward.domain.model;


import jakarta.persistence.Embeddable;

import java.util.UUID;

@Embeddable
public final class AvailabilityId {

    private final UUID id;

    private AvailabilityId() {
        this.id = UUID.randomUUID();
    }

    public AvailabilityId(UUID id) {
        if (id == null) {
            throw new IllegalArgumentException("AvailabilityId cannot be null");
        }
        this.id = id;
    }

    public static AvailabilityId generate() {
        return new AvailabilityId();
    }

    public static AvailabilityId of(UUID id) {
        return new AvailabilityId(id);
    }

    public UUID getValue() {
        return id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        AvailabilityId that = (AvailabilityId) o;

        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }

    // For logging and debugging
    @Override
    public String toString() {
        return id.toString();
    }
}package com.parkingexchange.reward.domain.model;

import com.parkingexchange.common.domain.model.BaseEntity;
import com.parkingexchange.reward.domain.event.AvailabilityCanceledEvent;
import com.parkingexchange.reward.domain.event.ReservationRequestCreatedEvent;
import com.parkingexchange.reward.domain.event.ReservationRequestRespondedEvent;
import com.parkingexchange.reward.domain.event.SpotTakenConfirmedEvent;
import jakarta.persistence.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotNull;

import java.time.LocalDateTime;
import java.util.*;


/**
 * Aggregate Root Entity representing the availability of a parking spot.
 */
@Entity
public class Availability extends BaseEntity {

    @EmbeddedId
    private AvailabilityId availabilityId;

    @NotNull
    @Column(name = "leaving_user_id", nullable = false)
    private UUID leavingUserId;

    @NotNull
    @Valid
    @Embedded
    private Location location;

    @NotNull
    @Future(message = "Estimated leaving time must be in the future")
    @Column(name = "estimated_leaving_time", nullable = false)
    private LocalDateTime estimatedLeavingTime;

    @NotNull
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private AvailabilityStatus status;

    @Column(name = "reserved_by_user_id")
    private UUID reservedByUserId;

    @OneToMany(mappedBy = "availability", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<ReservationRequest> reservationRequests = new ArrayList<>();

    protected Availability() {
        // Required by JPA
    }

    private Availability(AvailabilityId availabilityId,
                         UUID leavingUserId,
                         Location location,
                         LocalDateTime estimatedLeavingTime) {
        this.setAvailabilityId(availabilityId);
        this.setLeavingUserId(leavingUserId);
        this.setLocation(location);
        this.setEstimatedLeavingTime(estimatedLeavingTime);
        this.setStatus(AvailabilityStatus.AVAILABLE);
    }

    // Static factory method for creation
    public static Availability create(UUID leavingUserId, Location location, LocalDateTime estimatedLeavingTime) {
        AvailabilityId availabilityId = AvailabilityId.generate();
        return new Availability(availabilityId, leavingUserId, location, estimatedLeavingTime);
    }

    // Business methods

    public ReservationRequest createReservationRequest(UUID arrivingUserId, String message) {
        // Check if there's already a pending request from this user
        boolean hasExistingRequest = reservationRequests.stream()
                .anyMatch(request -> request.getArrivingUserId().equals(arrivingUserId)
                        && request.getStatus() == RequestStatus.PENDING);

        assertStateFalse(hasExistingRequest, "You already have a pending reservation request for this availability.");

        // Create a new reservation request
        ReservationRequest request = ReservationRequest.create(
                arrivingUserId,
                this.leavingUserId,
                message,
                this
        );

        // Add the request to the collection
        this.reservationRequests.add(request);

        // Update status if necessary
        this.requestReservation(arrivingUserId);

        this.publish(new ReservationRequestCreatedEvent(
                this.availabilityId.getValue(),
                request.getRequestId(),
                arrivingUserId,
                message
        ));

        return request;
    }

    public void respondToReservationRequest(UUID requestId, UUID leavingUserId, boolean accepted) {
        assertArgumentTrue(this.leavingUserId.equals(leavingUserId),
                "You do not have permission to respond to this request");

        ReservationRequest request = this.reservationRequests.stream()
                .filter(r -> r.getRequestId().equals(requestId))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Reservation request not found"));

        request.respond(leavingUserId, accepted);

        this.publish(new ReservationRequestRespondedEvent(
                this.availabilityId.getValue(),
                requestId,
                accepted
        ));

        if (accepted) {
            this.reserve(request.getArrivingUserId());
        } else {
            this.makeAvailableAfterRejection();
        }
    }

    public void requestReservation(UUID arrivingUserId) {
        assertArgumentTrue(this.status.equals(AvailabilityStatus.AVAILABLE),
                "Spot is not available for reservation");
        assertArgumentNotNull(arrivingUserId, "Arriving user ID cannot be null");
        this.setStatus(AvailabilityStatus.REQUEST_PENDING);
    }

    /**
     * Reserves this availability for a given arriving user.
     *
     * @param arrivingUserId the ID of the arriving user
     * @throws IllegalArgumentException if the spot is not available or the arrivingUserId is null
     */
    public void reserve(UUID arrivingUserId) {
        this.assertArgumentTrue(this.getStatus().equals(AvailabilityStatus.REQUEST_PENDING),
                "Only spots with a reqeust pending can be reserved");
        this.assertArgumentNotNull(arrivingUserId, "ArrivingUserId cannot be null");
        this.setReservedByUserId(arrivingUserId);
        this.setStatus(AvailabilityStatus.RESERVED);
    }

    private void setStatus(AvailabilityStatus aStatus){
        this.assertArgumentNotNull(aStatus, "status cannot be null");
        this.status = aStatus;
    }

    /**
     * Makes the availability available again after a reservation request is rejected.
     *
     * This method ensures that the availability's status is updated appropriately
     * and any business rules are enforced.
     */
    public void makeAvailableAfterRejection() {
        assertStateTrue(this.status == AvailabilityStatus.REQUEST_PENDING,
                "Availability status must be REQUEST_PENDING to make it available again.");
        this.setStatus(AvailabilityStatus.AVAILABLE);
    }

    /**
     * Confirms that the spot has been taken by the arriving user.
     *
     * @param arrivingUserId the ID of the arriving user
     * @throws IllegalArgumentException if the spot is not reserved or reserved by another user
     */
    public void confirmTaken(UUID arrivingUserId) {
        this.assertArgumentTrue(this.getStatus().equals(AvailabilityStatus.RESERVED),
                "Only reserved spots can be confirmed as taken");
        this.assertArgumentTrue(this.getReservedByUserId().equals(arrivingUserId),
                "This spot is not reserved by the provided user");
        this.setStatus(AvailabilityStatus.TAKEN);

        this.publish(new SpotTakenConfirmedEvent(
                this.availabilityId.getValue(),
                arrivingUserId
        ));
    }

    /**
     * Cancels this availability.
     *
     * <p>This method can be called by the leaving user.
     * It checks for the necessary permissions and updates the status accordingly.</p>
     *
     * @param userId the ID of the user attempting to cancel
     * @throws IllegalStateException if the user does not have permission or the availability is already canceled
     */

    public void cancel(UUID userId) {
        boolean hasPermission = this.leavingUserId.equals(userId);
        assertStateTrue(hasPermission, "You do not have permission to cancel this availability");
        assertStateFalse(this.status == AvailabilityStatus.CANCELED, "Availability is already canceled");
        this.setStatus(AvailabilityStatus.CANCELED);

        // Cancel all pending reservation requests
        for (ReservationRequest request : reservationRequests) {
            if (request.getStatus() == RequestStatus.PENDING) {
                request.cancel();
            }
        }

        this.publish(new AvailabilityCanceledEvent(
                this.availabilityId.getValue(),
                userId
        ));
    }

    // Getters and Setters with validation where applicable

    /**
     * Returns the unique identifier of this {@code Availability}.
     *
     * @return the {@code AvailabilityId}
     */
    public AvailabilityId getAvailabilityId() {
        return availabilityId;
    }

    private void setAvailabilityId(AvailabilityId availabilityId) {
        this.assertArgumentNotNull(availabilityId, "AvailabilityId cannot be null");
        this.availabilityId = availabilityId;
    }

    /**
     * Returns the ID of the user who is leaving the parking spot.
     *
     * @return the leaving user's ID
     */
    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    private void setLeavingUserId(UUID leavingUserId) {
        this.assertArgumentNotNull(leavingUserId, "LeavingUserId cannot be null");
        this.leavingUserId = leavingUserId;
    }

    /**
     * Returns the ID of the user who has reserved this spot, if any.
     *
     * @return the reserving user's ID, or null if not reserved
     */
    public UUID getReservedByUserId() {
        return reservedByUserId;
    }

    private void setReservedByUserId(UUID reservedByUserId) {
        this.assertArgumentNotNull(reservedByUserId, "ReservedByUserId cannot be null");
        this.reservedByUserId = reservedByUserId;
    }

    /**
     * Returns the location of the parking spot.
     *
     * @return the {@code Location}
     */
    public Location getLocation() {
        return location;
    }

    private void setLocation(Location location) {
        this.assertArgumentNotNull(location, "Location cannot be null");
        this.location = location;
    }

    /**
     * Returns the estimated time when the user will leave the spot.
     *
     * @return the estimated leaving time
     */
    public LocalDateTime getEstimatedLeavingTime() {
        return estimatedLeavingTime;
    }

    private void setEstimatedLeavingTime(LocalDateTime estimatedLeavingTime) {
        this.assertArgumentNotNull(estimatedLeavingTime, "Estimated leaving time cannot be null");
        this.assertArgumentTrue(estimatedLeavingTime.isAfter(LocalDateTime.now()),
                "Estimated leaving time must be in the future");
        this.estimatedLeavingTime = estimatedLeavingTime;
    }

    public List<ReservationRequest> getReservationRequests() {
        return Collections.unmodifiableList(reservationRequests);
    }

    /**
     * Returns the current status of this availability.
     *
     * @return the {@code AvailabilityStatus}
     */
    public AvailabilityStatus getStatus() {
        return status;
    }

    // equals and hashCode based on 'availabilityId'

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Availability that = (Availability) o;

        return availabilityId.equals(that.availabilityId);
    }

    @Override
    public int hashCode() {
        return availabilityId.hashCode();
    }

    @Override
    public String toString() {
        return "Availability{" +
                "availabilityId=" + availabilityId +
                ", leavingUserId=" + leavingUserId +
                ", location=" + location +
                ", estimatedLeavingTime=" + estimatedLeavingTime +
                ", status=" + status +
                ", reservedByUserId=" + reservedByUserId +
                '}';
    }
}package com.parkingexchange.reward.domain.model;

public enum AvailabilityStatus {
    AVAILABLE,
    REQUEST_PENDING,
    RESERVED,
    TAKEN,
    CANCELED
}
package com.parkingexchange.reward.domain.model;


import com.parkingexchange.common.AssertionConcern;
import jakarta.persistence.Embeddable;

import java.util.Objects;

/**
 * Immutable Value Object representing a geographical location.
 *
 * <p>This class encapsulates latitude and longitude coordinates, ensuring they are within valid ranges.</p>
 *
 * <p>Instances of this class are immutable and can be safely shared. The class uses assertion methods from
 * {@link AssertionConcern} to validate the input parameters.</p>
 */
@Embeddable
public class Location extends AssertionConcern {

    private final double latitude;
    private final double longitude;

    /**
     * Protected no-argument constructor for JPA.
     *
     * <p>Sets the latitude and longitude to 0.0 by default. This constructor should not be used directly.</p>
     */
    protected Location() {
        // Required by JPA
        this.latitude = 0.0;
        this.longitude = 0.0;
    }

    /**
     * Constructs a new {@code Location} with the specified latitude and longitude.
     *
     * @param latitude  the latitude coordinate, must be between -90 and 90 degrees
     * @param longitude the longitude coordinate, must be between -180 and 180 degrees
     * @throws IllegalArgumentException if the latitude or longitude are out of valid ranges
     */
    public Location(double latitude, double longitude) {
        // Validate latitude range
        this.assertArgumentRange(latitude, -90.0, 90.0,
                "Latitude must be between -90 and 90 degrees");

        // Validate longitude range
        this.assertArgumentRange(longitude, -180.0, 180.0,
                "Longitude must be between -180 and 180 degrees");

        this.latitude = latitude;
        this.longitude = longitude;
    }

    /**
     * Returns the latitude coordinate of this {@code Location}.
     *
     * @return the latitude value
     */
    public double getLatitude() {
        return latitude;
    }

    /**
     * Returns the longitude coordinate of this {@code Location}.
     *
     * @return the longitude value
     */
    public double getLongitude() {
        return longitude;
    }

    /**
     * Determines whether this location is equal to another object.
     *
     * <p>Two locations are considered equal if they have the same latitude and longitude values.</p>
     *
     * @param o the reference object with which to compare
     * @return {@code true} if this location is the same as the argument; {@code false} otherwise
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Location location = (Location) o;

        return Double.compare(location.latitude, latitude) == 0 &&
                Double.compare(location.longitude, longitude) == 0;
    }

    /**
     * Returns a hash code value for this location.
     *
     * <p>This method is supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.</p>
     *
     * @return the hash code value for this location
     */
    @Override
    public int hashCode() {
        return Objects.hash(latitude, longitude);
    }

    /**
     * Returns a string representation of this location.
     *
     * <p>The string representation consists of the latitude and longitude enclosed in parentheses.</p>
     *
     * @return a string representation of this location
     */
    @Override
    public String toString() {
        return "Location(" + "latitude=" + latitude + ", longitude=" + longitude + ')';
    }
}package com.parkingexchange.reward.domain.model;

public enum RequestStatus {
    PENDING,
    ACCEPTED,
    REJECTED,
    CANCELED
}package com.parkingexchange.reward.domain.model;

import com.parkingexchange.common.AssertionConcern;
import jakarta.persistence.*;

import java.time.LocalDateTime;
import java.util.UUID;

/**
 * The {@code ReservationRequest} class represents a reservation request in the parking exchange system.
 * It models the interaction between an arriving user who wants to reserve a parking spot and a leaving user
 * who is offering the spot. The class includes details such as the users involved, the status of the request,
 * timestamps of when the request was made and responded to, any messages included with the request, and
 * the associated parking availability.
 *
 * <p>This class is an entity managed by JPA (Java Persistence API), making it persistable in a relational database.</p>
 */

@Entity
public class ReservationRequest extends AssertionConcern {

    @Id
    private UUID requestId;

    /**
     * Identifier of the user who is arriving and making the reservation request.
     */
    @Column(nullable = false)
    private UUID arrivingUserId;

    /**
     * Identifier of the user who is leaving and offering the parking spot.
     */
    @Column(nullable = false)
    private UUID leavingUserId;

    /**
     * Current status of the reservation request.
     */
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private RequestStatus status;

    /**
     * Timestamp when the reservation request was created.
     */
    @Column(nullable = false)
    private LocalDateTime requestedAt;

    /**
     * Timestamp when the reservation request was responded to.
     */
    private LocalDateTime respondedAt;

    /**
     * Optional message included with the reservation request.
     */
    private String message;

    /**
     * The associated parking availability related to this request.
     */
    @ManyToOne
    private Availability availability;

    /**
     * Protected constructor for JPA use only.
     */
    protected ReservationRequest() {
    }

    /**
     * Creates a new reservation request.
     *
     * @param arrivingUserId the UUID of the arriving user
     * @param leavingUserId  the UUID of the leaving user
     * @param message        an optional message for the request
     * @param availability   the associated parking availability
     * @return a new instance of {@code ReservationRequest}
     */
    public static ReservationRequest create(UUID arrivingUserId, UUID leavingUserId,
                                            String message, Availability availability) {
        return new ReservationRequest(arrivingUserId, leavingUserId, message, availability);
    }

    /**
     * Private constructor to initialize a new reservation request with the provided details.
     *
     * @param arrivingUserId the UUID of the arriving user
     * @param leavingUserId  the UUID of the leaving user
     * @param message        an optional message for the request
     * @param availability   the associated parking availability
     */
    private ReservationRequest(UUID arrivingUserId, UUID leavingUserId,
                               String message, Availability availability) {
        this.setRequestId(UUID.randomUUID());
        this.setArrivingUserId(arrivingUserId);
        this.setLeavingUserId(leavingUserId);
        this.setStatus(RequestStatus.PENDING);
        this.setRequestedAt(LocalDateTime.now());
        this.setMessage(message);
        this.setAvailability(availability);
    }
    // Business methods

    private void setRequestId(UUID requestId){
        this.requestId = requestId;
    }
    /**
     * Allows the leaving user to respond to the reservation request, updating the status to accepted or rejected
     * and recording the response time.
     *
     * @param leavingUserId the UUID of the leaving user responding to the request
     * @param accepted      {@code true} if the request is accepted; {@code false} otherwise
     * @throws IllegalArgumentException if the leavingUserId does not match or the request is not pending
     */
    public void respond(UUID leavingUserId, boolean accepted) {
        assertArgumentTrue(this.leavingUserId.equals(leavingUserId), "You do not have permission to respond to this request");
        assertArgumentTrue(this.status.equals(RequestStatus.PENDING), "Request has already been responded to");

        this.setRespondedAt(LocalDateTime.now());
        this.setStatus(accepted ? RequestStatus.ACCEPTED : RequestStatus.REJECTED);
    }

    /**
     * Allows the arriving user to cancel the reservation request if it's still pending, updating the status
     * to canceled and recording the cancellation time.
     *
     * @throws IllegalArgumentException if the request is not pending
     */
    public void cancel() {
        assertArgumentTrue(this.status == RequestStatus.PENDING, "Only pending requests can be canceled.");
        this.setStatus(RequestStatus.CANCELED);
        this.setRespondedAt(LocalDateTime.now());
    }

    // Getters and Setters with validation

    /**
     * Returns the unique identifier for the reservation request.
     *
     * @return the {@code requestId}
     */
    public UUID getRequestId() {
        return requestId;
    }

    private void setAvailability(Availability availability) {
        this.availability = availability;
    }

    /**
     * Returns the associated availability related to this request.
     *
     * @return the {@code availability}
     */
    public Availability getAvailability() {
        return availability;
    }

    /**
     * Returns the UUID of the arriving user.
     *
     * @return the {@code arrivingUserId}
     */
    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

    private void setArrivingUserId(UUID arrivingUserId) {
        assertArgumentNotNull(arrivingUserId, "ArrivingUserId cannot be null");
        this.arrivingUserId = arrivingUserId;
    }

    /**
     * Returns the UUID of the leaving user.
     *
     * @return the {@code leavingUserId}
     */
    public UUID getLeavingUserId() {
        return leavingUserId;
    }

    private void setLeavingUserId(UUID leavingUserId) {
        assertArgumentNotNull(leavingUserId, "LeavingUserId cannot be null");
        this.leavingUserId = leavingUserId;
    }

    /**
     * Returns the current status of the reservation request.
     *
     * @return the {@code status}
     */
    public RequestStatus getStatus() {
        return status;
    }

    private void setStatus(RequestStatus status) {
        assertArgumentNotNull(status, "Status cannot be null");
        this.status = status;
    }

    /**
     * Returns the timestamp when the reservation request was created.
     *
     * @return the {@code requestedAt} timestamp
     */
    public LocalDateTime getRequestedAt() {
        return requestedAt;
    }

    private void setRequestedAt(LocalDateTime requestedAt) {
        this.requestedAt = requestedAt;
    }

    /**
     * Returns the timestamp when the reservation request was responded to or canceled.
     *
     * @return the {@code respondedAt} timestamp
     */
    public LocalDateTime getRespondedAt() {
        return respondedAt;
    }

    private void setRespondedAt(LocalDateTime respondedAt) {
        this.respondedAt = respondedAt;
    }

    /**
     * Returns the optional message included with the reservation request.
     *
     * @return the {@code message}
     */
    public String getMessage() {
        return message;
    }

    private void setMessage(String message) {
        this.message = message;
    }
}package com.parkingexchange.reward.domain.repository;

import com.parkingexchange.reward.domain.model.Availability;
import com.parkingexchange.reward.domain.model.AvailabilityId;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface AvailabilityRepository {

    Availability save(Availability availability);

    Optional<Availability> findByAvailabilityId(AvailabilityId availabilityId);

    List<Availability> findAvailableInArea(double latitude, double longitude, double radius);

    Optional<Availability> findByReservationRequestId(@Param("requestId") UUID requestId);
    // Additional methods as needed
}package com.parkingexchange.reward.infrastructure.config;

import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ModelMapperConfig {

    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}package com.parkingexchange.reward.infrastructure.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient userServiceWebClient(@Value("${user.context.base.url}") String baseUrl) {
        return WebClient.builder()
                .baseUrl(baseUrl)
                .build();
    }
}package com.parkingexchange.reward.infrastructure.interfaces.controller;

import com.parkingexchange.reward.application.service.AvailabilityApplicationService;
import com.parkingexchange.reward.application.dto.*;
import com.parkingexchange.reward.domain.model.AvailabilityId;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api/availability")
public class AvailabilityController {

    private final AvailabilityApplicationService availabilityService;

    public AvailabilityController(AvailabilityApplicationService availabilityService) {
        this.availabilityService = availabilityService;
    }

    @PostMapping
    public AvailabilityResponse reportAvailability(@RequestBody AvailabilityRequest request) {
        return availabilityService.reportAvailability(request);
    }

    @GetMapping("/search")
    public List<AvailabilityResponse> searchAvailableSpots(@RequestParam double latitude,
                                                           @RequestParam double longitude,
                                                           @RequestParam double radius) {
        SearchCriteria criteria = new SearchCriteria();
        criteria.setLatitude(latitude);
        criteria.setLongitude(longitude);
        criteria.setRadius(radius);
        return availabilityService.searchAvailableSpots(criteria);
    }

    @PostMapping("/{availabilityId}/request")
    public ReservationRequestResponse requestReservation(@PathVariable UUID availabilityId,
                                                         @RequestBody ReservationRequestDto requestDto) {
        return availabilityService.requestReservation(AvailabilityId.of(availabilityId), requestDto);
    }

    @PostMapping("/requests/{requestId}/respond")
    public ReservationRequestResponse respondToReservation(@PathVariable UUID requestId,
                                                           @RequestBody ReservationResponseDto responseDto) {
        return availabilityService.respondToReservation(requestId, responseDto);
    }

    @PostMapping("/{availabilityId}/confirm")
    public AvailabilityResponse confirmSpotTaken(@PathVariable UUID availabilityId,
                                                 @RequestBody ConfirmSpotTakenRequest request) {
        return availabilityService.confirmSpotTaken(AvailabilityId.of(availabilityId), request);
    }

    @PostMapping("/{availabilityId}/cancel")
    public AvailabilityResponse cancelAvailability(@PathVariable UUID availabilityId,
                                                   @RequestBody CancelAvailabilityRequest request) {
        return availabilityService.cancelAvailability(AvailabilityId.of(availabilityId), request);
    }
}package com.parkingexchange.reward.infrastructure.interfaces.dto;

public class UserExistsResponse {

    private boolean exists;

    // Getters and Setters

    public boolean isExists() {
        return exists;
    }

    public void setExists(boolean exists) {
        this.exists = exists;
    }
}package com.parkingexchange.reward.infrastructure.interfaces.dto;


public class UserReputationResponse {

    private double reputation;

    // Getters and Setters

    public double getReputation() {
        return reputation;
    }

    public void setReputation(double reputation) {
        this.reputation = reputation;
    }
}
package com.parkingexchange.parking.infrastructure.resporitory;

import com.parkingexchange.reward.domain.model.Availability;
import com.parkingexchange.reward.domain.model.AvailabilityId;
import com.parkingexchange.reward.domain.repository.AvailabilityRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface JpaAvailabilityRepository extends AvailabilityRepository, JpaRepository<Availability, AvailabilityId> {

    @Query("SELECT a FROM Availability a WHERE "
            + "(6371 * acos(cos(radians(:latitude)) * cos(radians(a.location.latitude)) "
            + "* cos(radians(a.location.longitude) - radians(:longitude)) "
            + "+ sin(radians(:latitude)) * sin(radians(a.location.latitude)))) < :radius")
    List<Availability> findAvailableInArea(
            @Param("latitude") double latitude,
            @Param("longitude") double longitude,
            @Param("radius") double radius);

    @Query("SELECT a FROM Availability a JOIN a.reservationRequests r WHERE r.requestId = :requestId")
    Optional<Availability> findByReservationRequestId(@Param("requestId") UUID requestId);


}package com.parkingexchange.reward;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@ComponentScan(basePackages = {"com.parkingexchange"})
@EnableJpaRepositories(basePackages = "com.parkingexchange")
@EntityScan(basePackages = "com.parkingexchange")
public class ParkingApplication {

	public static void main(String[] args) {
		SpringApplication.run(ParkingApplication.class, args);
	}

}
package com.parkingexchange.reward;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ParkingApplicationTests {

	@Test
	void contextLoads() {
	}

}
package com.parkingexchange.parking.application.dto.mapper;

import com.parkingexchange.parking.application.dto.AvailabilityResponse;
import com.parkingexchange.parking.application.dto.ReservationRequestResponse;
import com.parkingexchange.parking.domain.model.Availability;
import com.parkingexchange.parking.domain.model.ReservationRequest;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-09-29T18:39:11+0200",
    comments = "version: 1.6.2, compiler: javac, environment: Java 21 (Oracle Corporation)"
)
@Component
public class AvailabilityMapperImpl implements AvailabilityMapper {

    @Override
    public AvailabilityResponse toResponse(Availability availability) {
        if ( availability == null ) {
            return null;
        }

        AvailabilityResponse availabilityResponse = new AvailabilityResponse();

        availabilityResponse.setAvailabilityId( map( availability.getAvailabilityId() ) );
        availabilityResponse.setLeavingUserId( availability.getLeavingUserId() );
        availabilityResponse.setEstimatedLeavingTime( availability.getEstimatedLeavingTime() );
        availabilityResponse.setStatus( availability.getStatus() );
        availabilityResponse.setReservedByUserId( availability.getReservedByUserId() );
        availabilityResponse.setReservationRequests( reservationRequestListToReservationRequestResponseList( availability.getReservationRequests() ) );

        return availabilityResponse;
    }

    @Override
    public ReservationRequestResponse toReservationResponse(ReservationRequest request) {
        if ( request == null ) {
            return null;
        }

        ReservationRequestResponse reservationRequestResponse = new ReservationRequestResponse();

        reservationRequestResponse.setRequestId( request.getRequestId() );
        reservationRequestResponse.setArrivingUserId( request.getArrivingUserId() );
        reservationRequestResponse.setLeavingUserId( request.getLeavingUserId() );
        reservationRequestResponse.setStatus( request.getStatus() );
        reservationRequestResponse.setRequestedAt( request.getRequestedAt() );
        reservationRequestResponse.setRespondedAt( request.getRespondedAt() );
        reservationRequestResponse.setMessage( request.getMessage() );

        return reservationRequestResponse;
    }

    protected List<ReservationRequestResponse> reservationRequestListToReservationRequestResponseList(List<ReservationRequest> list) {
        if ( list == null ) {
            return null;
        }

        List<ReservationRequestResponse> list1 = new ArrayList<ReservationRequestResponse>( list.size() );
        for ( ReservationRequest reservationRequest : list ) {
            list1.add( toReservationResponse( reservationRequest ) );
        }

        return list1;
    }
}
package com.parkingexchange.reward.application.dto;

import com.parkingexchange.reward.domain.model.RewardStatus;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

public class RewardResponseDto {

    private UUID rewardId;
    private UUID fromUserId;
    private UUID toUserId;
    private UUID availabilityId;
    private BigDecimal amount;
    private RewardStatus status;
    private LocalDateTime createdAt;
    private LocalDateTime completedAt;

    // Getters and Setters

    public UUID getRewardId() {
        return rewardId;
    }

    public void setRewardId(UUID rewardId) {
        this.rewardId = rewardId;
    }

    public UUID getFromUserId() {
        return fromUserId;
    }

    public void setFromUserId(UUID fromUserId) {
        this.fromUserId = fromUserId;
    }

    public UUID getToUserId() {
        return toUserId;
    }

    public void setToUserId(UUID toUserId) {
        this.toUserId = toUserId;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public void setAvailabilityId(UUID availabilityId) {
        this.availabilityId = availabilityId;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }

    public RewardStatus getStatus() {
        return status;
    }

    public void setStatus(RewardStatus status) {
        this.status = status;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public LocalDateTime getCompletedAt() {
        return completedAt;
    }

    public void setCompletedAt(LocalDateTime completedAt) {
        this.completedAt = completedAt;
    }
}package com.parkingexchange.reward.application.mapper;


import com.parkingexchange.reward.application.dto.RewardResponseDto;
import com.parkingexchange.reward.domain.model.Reward;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface RewardMapper {

    RewardResponseDto toDto(Reward reward);
}// RewardApplicationServiceImpl.java
package com.parkingexchange.reward.application.service;

import com.parkingexchange.reward.application.dto.RewardResponseDto;
import com.parkingexchange.reward.application.mapper.RewardMapper;
import com.parkingexchange.reward.domain.model.Reward;
import com.parkingexchange.reward.domain.repository.RewardRepository;
import com.parkingexchange.reward.domain.service.RewardCalculationService;
import com.parkingexchange.reward.domain.service.RewardProcessingService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.UUID;

@Service
public class RewardApplicationServiceImpl implements RewardApplicationService {

    private final RewardRepository rewardRepository;
    private final RewardCalculationService rewardCalculatorService;
    private final RewardProcessingService rewardProcessingService;
    private final RewardMapper rewardMapper;
    private final Logger log;

    @Autowired
    public RewardApplicationServiceImpl(
            RewardRepository rewardRepository,
            RewardCalculationService rewardCalculatorService,
            RewardProcessingService rewardProcessingService,
            RewardMapper rewardMapper,
            Logger log
    ) {
        this.rewardRepository = rewardRepository;
        this.rewardCalculatorService = rewardCalculatorService;
        this.rewardProcessingService = rewardProcessingService;
        this.rewardMapper = rewardMapper;
        this.log = log;
    }

    @Transactional
    @Override
    public RewardResponseDto processReward(UUID availabilityId, UUID fromUserId, UUID toUserId) {
        try {
            // Validate inputs
            if (availabilityId == null || fromUserId == null || toUserId == null) {
                throw new IllegalArgumentException("All input parameters must be non-null");
            }

            // Calculate the reward amount using the domain service
            BigDecimal amount = rewardCalculatorService.calculateRewardAmount(availabilityId);

            // Create and save the reward entity
            Reward reward = Reward.create(fromUserId, toUserId, availabilityId, amount);
            rewardRepository.save(reward);

            // Process the reward payment
            boolean success = rewardProcessingService.processPayment(reward);

            // Update reward status based on payment processing result
            if (success) {
                reward.markAsCompleted();
                log.info("Reward processed successfully for rewardId: {}", reward.getRewardId());
            } else {
                reward.markAsFailed();
                log.warn("Reward processing failed for rewardId: {}", reward.getRewardId());
            }

            // Save the updated reward status
            rewardRepository.save(reward);

            // Map the reward entity to DTO and return
            return rewardMapper.toDto(reward);

        } catch (Exception e) {
            log.error("Error processing reward for availabilityId: {}", availabilityId, e);
            // Handle exception appropriately, possibly rethrow a custom exception
            throw e;
        }
    }
}package com.parkingexchange.reward.application.service;


import com.parkingexchange.reward.application.dto.RewardResponseDto;

import java.util.UUID;

public interface RewardApplicationService {

    RewardResponseDto processReward(UUID availabilityId, UUID fromUserId, UUID toUserId);
}package com.parkingexchange.reward.application.service;

import com.parkingexchange.reward.domain.model.Reward;
import com.parkingexchange.reward.domain.service.RewardProcessingService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RewardProcessingServiceImpl implements RewardProcessingService {

    @Override
    public boolean processPayment(Reward reward) {
        // Integrate with external payment gateway
        // Implement retries, exception handling, and circuit breakers

        // Placeholder implementation
        try {
            // Simulate payment processing
            // If using a payment gateway, handle exceptions and failures

            // Payment successful
            return true;

        } catch (Exception e) {
            log.error("Payment processing failed for rewardId: {}", reward.getRewardId(), e);
            return false;
        }
    }
}package com.parkingexchange.reward.domain.event;

import com.parkingexchange.common.domain.model.DomainEvent;

import java.time.Instant;
import java.util.UUID;

public class RewardCompletedEvent implements DomainEvent {

    private final UUID rewardId;
    private final Instant occurredOn;
    private final String moduleName = "reward"; // TODO you should't set this manually

    public RewardCompletedEvent(UUID rewardId) {
        this.rewardId = rewardId;
        this.occurredOn = Instant.now();
    }

    public UUID getRewardId() {
        return rewardId;
    }

    @Override
    public int eventVersion() {
        return 0;
    }

    @Override
    public Instant occurredOn() {
        return occurredOn;
    }

    @Override
    public String moduleName() {
        return moduleName;
    }
}package com.parkingexchange.reward.domain.model;


import com.parkingexchange.common.domain.model.BaseEntity;
import com.parkingexchange.reward.domain.event.RewardCompletedEvent;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Table(name = "rewards")
public class Reward extends BaseEntity {

    @Id
    @Column(name = "reward_id", nullable = false)
    private UUID rewardId;

    @NotNull
    @Column(name = "from_user_id", nullable = false)
    private UUID fromUserId;

    @NotNull
    @Column(name = "to_user_id", nullable = false)
    private UUID toUserId;

    @NotNull
    @Column(name = "availability_id", nullable = false)
    private UUID availabilityId;

    @NotNull
    @Column(name = "amount", nullable = false)
    private BigDecimal amount;

    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false)
    private RewardStatus status;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;

    @Column(name = "completed_at")
    private LocalDateTime completedAt;

    protected Reward() {
        // For JPA
    }

    private Reward(UUID fromUserId, UUID toUserId, UUID availabilityId, BigDecimal amount) {
        this.rewardId = UUID.randomUUID();
        this.fromUserId = fromUserId;
        this.toUserId = toUserId;
        this.availabilityId = availabilityId;
        this.amount = amount;
        this.status = RewardStatus.PENDING;
        this.createdAt = LocalDateTime.now();
    }

    public static Reward create(UUID fromUserId, UUID toUserId, UUID availabilityId, BigDecimal amount) {
        return new Reward(fromUserId, toUserId, availabilityId, amount);
    }

    // Business Methods

    public void markAsCompleted() {
        this.status = RewardStatus.COMPLETED;
        this.completedAt = LocalDateTime.now();
        this.publish(new RewardCompletedEvent(this.getRewardId()));
    }

    public void markAsFailed() {
        this.status = RewardStatus.FAILED;
    }

    // Getters (no setters for immutability)

    public UUID getRewardId() {
        return rewardId;
    }

    public UUID getFromUserId() {
        return fromUserId;
    }

    public UUID getToUserId() {
        return toUserId;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public RewardStatus getStatus() {
        return status;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getCompletedAt() {
        return completedAt;
    }

    // Override equals and hashCode based on rewardId

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Reward reward = (Reward) o;

        return rewardId.equals(reward.rewardId);
    }

    @Override
    public int hashCode() {
        return rewardId.hashCode();
    }
}package com.parkingexchange.reward.domain.model;

public enum RewardStatus {
    PENDING,
    COMPLETED,
    FAILED
}
package com.parkingexchange.reward.domain.repository;

import com.parkingexchange.reward.domain.model.Reward;

import java.util.Optional;
import java.util.UUID;

public interface RewardRepository {

    Reward save(Reward reward);

    Optional<Reward> findByRewardId(UUID rewardId);

    // Additional methods as needed
}package com.parkingexchange.reward.domain.service;

import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.UUID;

@Service
public class RewardCalculationServiceImpl implements RewardCalculationService {

    @Override
    public BigDecimal calculateRewardAmount(UUID availabilityId) {
        // Implement reward calculation logic

        // For example, base amount + demand factor + user reputation

        // Placeholder for actual logic
        return BigDecimal.valueOf(5.00);
    }
}package com.parkingexchange.reward.domain.service;

import java.math.BigDecimal;
import java.util.UUID;

public interface RewardCalculationService {

    BigDecimal calculateRewardAmount(UUID availabilityId);
}package com.parkingexchange.reward.domain.service;

import com.parkingexchange.reward.domain.model.Reward;

public interface RewardProcessingService {

    boolean processPayment(Reward reward);
}// RewardController.java
package com.parkingexchange.reward.infrastructure.interfaces.controller;

import com.parkingexchange.reward.application.dto.RewardResponseDto;
import com.parkingexchange.reward.application.service.RewardApplicationService;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/api/rewards")
public class RewardController {

    private final RewardApplicationService rewardApplicationService;

    public RewardController(RewardApplicationService rewardApplicationService) {
        this.rewardApplicationService = rewardApplicationService;
    }

    @PostMapping("/process")
    public RewardResponseDto processReward(
            @RequestParam UUID availabilityId,
            @RequestParam UUID fromUserId,
            @RequestParam UUID toUserId
    ) {
        return rewardApplicationService.processReward(availabilityId, fromUserId, toUserId);
    }
}package com.parkingexchange.reward.infrastructure.resporitory;

import com.parkingexchange.reward.domain.model.Reward;
import com.parkingexchange.reward.domain.repository.RewardRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface JpaAvailabilityRepository extends RewardRepository, JpaRepository<Reward, UUID> {


}package com.parkingexchange.reward;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@ComponentScan(basePackages = {"com.parkingexchange"})
@EnableJpaRepositories(basePackages = "com.parkingexchange")
@EntityScan(basePackages = "com.parkingexchange")
public class RewardApplication {

	public static void main(String[] args) {
		SpringApplication.run(RewardApplication.class, args);
	}

}
package com.parkingexchange.reward;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class RewardApplicationTests {

	@Test
	void contextLoads() {
	}

}
package com.parkingexchange.reward.application.mapper;

import com.parkingexchange.reward.application.dto.RewardResponseDto;
import com.parkingexchange.reward.domain.model.Reward;
import javax.annotation.processing.Generated;
import org.springframework.stereotype.Component;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-10-05T10:56:19+0200",
    comments = "version: 1.6.2, compiler: javac, environment: Java 21 (Oracle Corporation)"
)
@Component
public class RewardMapperImpl implements RewardMapper {

    @Override
    public RewardResponseDto toDto(Reward reward) {
        if ( reward == null ) {
            return null;
        }

        RewardResponseDto rewardResponseDto = new RewardResponseDto();

        rewardResponseDto.setRewardId( reward.getRewardId() );
        rewardResponseDto.setFromUserId( reward.getFromUserId() );
        rewardResponseDto.setToUserId( reward.getToUserId() );
        rewardResponseDto.setAvailabilityId( reward.getAvailabilityId() );
        rewardResponseDto.setAmount( reward.getAmount() );
        rewardResponseDto.setStatus( reward.getStatus() );
        rewardResponseDto.setCreatedAt( reward.getCreatedAt() );
        rewardResponseDto.setCompletedAt( reward.getCompletedAt() );

        return rewardResponseDto;
    }
}

How can we manage to make it process the reward when a available spot is confirmed?
Note that you can use the message using the core functionality of our company framework
for example this is used in another application
package org.parkapp.parking.infrastructure;

import org.parkapp.common.application.ExchangeListener;
import org.parkapp.common.application.PropertiesSerializer;
import org.parkapp.common.application.eventstore.EventSerializer;
import org.parkapp.common.application.eventstore.StoredEventService;
import org.parkapp.common.application.notifications.NotificationSerializer;
import org.parkapp.common.application.notifications.PublishedNotificationTrackerService;
import org.parkapp.Modules;
import org.parkapp.common.infrastructure.notification.RabbitConfigurationProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Properties;
import java.util.UUID;

@Component
public class RabbitmqParkingSpotCreatedEventListener extends ExchangeListener {


    private static String COMMAND = "org.parkapp.booking.application.bookingrequest.BookableResourceCreationCommand";
    private final EventSerializer eventSerializer;

    private final RabbitTemplate rabbitTemplate;

    private final PropertiesSerializer propertiesSerializer;
    private final RabbitConfigurationProperties rabbitConfigurationProperties;


    @Autowired
    public RabbitmqParkingSpotCreatedEventListener(EventSerializer eventSerializer,
                                                   NotificationSerializer notificationSerializer,
                                                   RabbitTemplate rabbitTemplate,
                                                   PublishedNotificationTrackerService publishedNotificationTrackerService,
                                                   StoredEventService storedEventService,
                                                   RabbitConfigurationProperties rabbitConfigurationProperties,
                                                   PropertiesSerializer propertiesSerializer) {
        this.eventSerializer = eventSerializer;
        this.rabbitTemplate = rabbitTemplate;
        this.rabbitConfigurationProperties = rabbitConfigurationProperties;
        this.propertiesSerializer = propertiesSerializer;
    }


    @Override
    @RabbitListener(id = "ParkingSpotCreatedEvent", queues = {"org.parkapp.parking.domain.parkingspot.ParkingSpotCreatedEvent"})
    public void filteredDispatch(String aTextMessage) {
        UUID uuid = UUID.randomUUID();
        Properties properties = new Properties();
        properties.put("uuid", uuid);
        rabbitTemplate.convertAndSend(rabbitConfigurationProperties.getExchange(Modules.BOOKING_MODULE_NAME), COMMAND, propertiesSerializer.serialize(properties));
    }

    @Override
    protected String[] listensTo() {
        return new String[]{
                "org.parkapp.parking.domain.parkingspot.ParkingSpotCreatedEvent",
        };
    }
}

package org.parkapp.parking.infrastructure.messaging.rabbitmq;

import org.parkapp.Modules;
import org.parkapp.common.infrastructure.notification.RabbitConfigurationProperties;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Configuration
public class ParkingQueueConfiguration {

  private final RabbitConfigurationProperties rabbitConfigurationProperties;
  static final String topicExchangeName = Modules.PARKING_MODULE_NAME;

  @Autowired
  public ParkingQueueConfiguration(RabbitConfigurationProperties rabbitConfigurationProperties) {
      this.rabbitConfigurationProperties = rabbitConfigurationProperties;
  }

    @Bean(name = "parkingExchange")
  TopicExchange exchange() {
    return new TopicExchange(rabbitConfigurationProperties.getExchange(topicExchangeName));
  }

  @Bean(name = "parkingBinding")
  Binding binding(@Qualifier("parkingSpotCreatedQueue") Queue queue, @Qualifier("parkingExchange") TopicExchange exchange) {
    return BindingBuilder.bind(queue).to(exchange).with(queue.getName());
  }

  @Bean(name = "parkingContainer")
  SimpleMessageListenerContainer container(ConnectionFactory connectionFactory) {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    return container;
  }


  @Bean(name = "parkingSpotCreatedQueue")
  public Queue parkingSpotCreatedQueue() {
    return new Queue("org.parkapp.parking.domain.parkingspot.ParkingSpotCreatedEvent", true, false, false);
  }
}

create also the configuration file if you can, and explain why you have configured it that way