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);
}// RewardRabbitMQConfig.java
package com.parkingexchange.reward.infrastructure.configuration.messagging;

import com.parkingexchange.common.infrastructure.configuration.RabbitMQCommonConfig;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RewardRabbitMQConfig extends RabbitMQCommonConfig {

    @Bean
    public Queue spotTakenConfirmedQueue() {
        return QueueBuilder.durable("com.parkingexchange.reward.domain.event.SpotTakenConfirmedEvent")
                .withArgument("x-dead-letter-exchange", "DeadLetterExchange")
                .build();
    }

    @Bean
    public Binding spotTakenConfirmedBinding(Queue spotTakenConfirmedQueue, TopicExchange rewardExchange) {
        return BindingBuilder.bind(spotTakenConfirmedQueue)
                .to(rewardExchange)
                .with("com.parkingexchange.reward.domain.event.SpotTakenConfirmedEvent");
    }



}// 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.messagging;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.parkingexchange.reward.application.service.RewardApplicationService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

@Component
public class SpotTakenConfirmedEventListener {

    private final RewardApplicationService rewardApplicationService;
    private final AvailabilityRepository availabilityRepository;
    private final ObjectMapper objectMapper;

    public SpotTakenConfirmedEventListener(RewardApplicationService rewardApplicationService,
                                           AvailabilityRepository availabilityRepository,
                                           ObjectMapper objectMapper) {
        this.rewardApplicationService = rewardApplicationService;
        this.availabilityRepository = availabilityRepository;
        this.objectMapper = objectMapper;
    }

    @RabbitListener(queues = "com.parkingexchange.reward.domain.event.SpotTakenConfirmedEvent")
    public void handleSpotTakenConfirmedEvent(String message) {
        try {
            SpotTakenConfirmedEvent event = objectMapper.readValue(message, SpotTakenConfirmedEvent.class);
            UUID availabilityId = event.getAvailabilityId();
            UUID arrivingUserId = event.getArrivingUserId();

            // Fetch leavingUserId from Availability
            Availability availability = availabilityRepository.findByAvailabilityId(AvailabilityId.of(availabilityId))
                    .orElseThrow(() -> new IllegalArgumentException("Availability not found"));
            UUID leavingUserId = availability.getLeavingUserId();

            // Process the reward
            rewardApplicationService.processReward(availabilityId, arrivingUserId, leavingUserId);

        } catch (IOException e) {
            // Handle exception appropriately
            e.printStackTrace();
        }
    }
}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;
    }
}



Can you make sure that a reward is given when a parking spot is taken ?
Make it compliant with good programming, DDD and the new modification should be relayable and production ready.
This is the description of what i want to relize

I'm trying to create a java application. I am in the system design phase following DDD principles. Can you provide an system design for the app? The app should be similar to Uber, but instead of selling rides, you sell parking spot (public parking spot). It means that if you are leaving you can signal on the app that you are leaving a parking spot, and the estimated time you leave, so another person can go there (if they are trying to find a spot) and take your place. You should be rewarded for this (maybe a variable amount)

Please note that reward and parking are two different microservices