com.parkingexchange.common.domain.model.process;

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

import java.time.Duration;
import java.time.Instant;

public abstract class AbstractProcess extends BaseEntity implements Process {


private static final long serialVersionUID = 1L;

private long allowableDuration;
private String description;
private ProcessId processId;
private ProcessCompletionType processCompletionType;
private Instant startTime;
private Instant timedOutDate;
private int totalRetriesPermitted;
private String module;

public AbstractProcess(
        ProcessId aProcessId,
        String aDescription,
        String module) {

    super();

    this.setDescription(aDescription);
    this.setProcessCompletionType(ProcessCompletionType.NotCompleted);
    this.setProcessId(aProcessId);
    this.setStartTime(Instant.now());
    this.setModule(module);
}

public AbstractProcess(
        ProcessId aProcessId,
        String aDescription,
        long anAllowableDuration,
        String module) {

    this(aProcessId, aDescription, module);

    this.setAllowableDuration(anAllowableDuration);
}

public AbstractProcess(
        ProcessId aProcessId,
        String aDescription,
        long anAllowableDuration,
        int aTotalRetriesPermitted,
        String module) {

    this(aProcessId, aDescription, anAllowableDuration, module);

    this.setTotalRetriesPermitted(aTotalRetriesPermitted);
}

public String getModule(){
    return this.module;
}

public void setModule(String module){
    this.module = module;
}

@Override
public long allowableDuration() {
    return this.allowableDuration;
}

@Override
public boolean canTimeout() {
    return this.allowableDuration() > 0;
}

@Override
public long currentDuration() {
    return this.calculateTotalCurrentDuration(Instant.now());
}

@Override
public String description() {
    return this.description;
}

@Override
public boolean didProcessingComplete() {
    return this.isCompleted() && !this.isTimedOut();
}

@Override
public void informTimeout(Instant aTimedOutDate) {
    this.assertStateTrue(
            this.hasProcessTimedOut(aTimedOutDate),
            "The date " + aTimedOutDate + " does not indicate a valid timeout.");

    this.setProcessCompletionType(ProcessCompletionType.TimedOut);
    this.setTimedOutDate(aTimedOutDate);
}

@Override
public boolean isCompleted() {
    return !this.notCompleted();
}

@Override
public boolean isTimedOut() {
    return this.timedOutDate() != null;
}

@Override
public boolean notCompleted() {
    return this.processCompletionType().equals(ProcessCompletionType.NotCompleted);
}

@Override
public ProcessCompletionType processCompletionType() {
    return this.processCompletionType;
}

@Override
public ProcessId processId() {
    return this.processId;
}

@Override
public Instant startTime() {
    return this.startTime;
}

@Override
public TimeConstrainedProcessTracker timeConstrainedProcessTracker() {
    this.assertStateTrue(this.canTimeout(), "Process does not timeout.");

    TimeConstrainedProcessTracker tracker =
            new TimeConstrainedProcessTracker(
                    this.processId(),
                    this.description(),
                    this.startTime(),
                    this.allowableDuration(),
                    this.totalRetriesPermitted(),
                    this.processTimedOutEventType().getName(),
                    this.getModule());

    return tracker;
}

@Override
public Instant timedOutDate() {
    return this.timedOutDate;
}

@Override
public long totalAllowableDuration() {
    long totalAllowableDuration = this.allowableDuration();
    long totalRetriesPermitted = this.totalRetriesPermitted();

    if (totalRetriesPermitted > 0) {
        totalAllowableDuration *= totalRetriesPermitted;
    }

    return totalAllowableDuration;
}

@Override
public int totalRetriesPermitted() {
    return this.totalRetriesPermitted;
}

@Override
public boolean equals(Object anObject) {
    boolean equalObjects = false;

    if (anObject != null && this.getClass() == anObject.getClass()) {
        AbstractProcess typedObject = (AbstractProcess) anObject;
        equalObjects = this.processId().equals(typedObject.processId());
    }

    return equalObjects;
}

@Override
public int hashCode() {
    int hashCodeValue =
            +(71547 * 953)
                    + this.processId().hashCode();

    return hashCodeValue;
}

@Override
public String toString() {
    return "AbstractProcess [id=" + getId() + "allowableDuration=" + allowableDuration
            + ", description=" + description + ", processId=" + processId
            + ", processCompletionType=" + processCompletionType + ", startTime=" + startTime
            + ", timedOutDate=" + timedOutDate
            + ", totalRetriesPermitted=" + totalRetriesPermitted + "]";
}

protected AbstractProcess() {
    super();
}

protected void completeProcess(ProcessCompletionType aProcessCompletionType) {
    if (!this.isCompleted() && this.completenessVerified()) {
        this.setProcessCompletionType(aProcessCompletionType);
    }
}

protected abstract boolean completenessVerified();

protected abstract Class<? extends ProcessTimedOut> processTimedOutEventType();

private long calculateTotalCurrentDuration(Instant aDateFollowingStartTime) {
    return Duration.between(this.startTime(), aDateFollowingStartTime).toMillis();
}

private boolean hasProcessTimedOut(Instant aTimedOutDate) {
    return this.calculateTotalCurrentDuration(aTimedOutDate) >=
            this.totalAllowableDuration();
}

private void setAllowableDuration(long anAllowableDuration) {
    this.assertArgumentTrue(
            anAllowableDuration > 0,
            "The allowable duration must be greater than zero.");

    this.allowableDuration = anAllowableDuration;
}

private void setDescription(String description) {
    this.description = description;
}

private void setProcessCompletionType(ProcessCompletionType aProcessCompletionType) {
    this.processCompletionType = aProcessCompletionType;
}

private void setProcessId(ProcessId aProcessId) {
    this.assertArgumentNotNull(aProcessId, "Process id must be provided.");

    this.processId = aProcessId;
}

private void setStartTime(Instant aStartTime) {
    this.startTime = aStartTime;
}

private void setTimedOutDate(Instant aTimedOutDate) {
    this.timedOutDate = aTimedOutDate;
}

private void setTotalRetriesPermitted(int aTotalRetriesPermitted) {
    this.totalRetriesPermitted = aTotalRetriesPermitted;
}
}
package com.parkingexchange.common.domain.model.process;

import com.parkingexchange.common.AssertionConcern;

import java.io.Serializable;

public class IdentifiedDomainObject extends AssertionConcern implements Serializable {


private static final long serialVersionUID = 1L;

private long id;

protected IdentifiedDomainObject() {
    super();

    this.setId(-1);
}

protected long getId() {
    return this.id;
}

private void setId(long anId) {
    this.id = anId;
}
}
package com.parkingexchange.common.domain.model.process;

import java.io.Serializable;
import java.util.UUID;

public final class ProcessId implements Serializable {


private static final long serialVersionUID = 1L;
private UUID id;

public static ProcessId existingProcessId(UUID anId) {
    ProcessId processId = new ProcessId(anId);

    return processId;
}

public static ProcessId newProcessId() {
    ProcessId processId =
            new ProcessId(UUID.randomUUID());

    return processId;
}

protected ProcessId(UUID anId) {
    this.id = anId;
}

public UUID getId(){
    return this.id;
}
}
package com.parkingexchange.common.domain.model.process;

import java.time.Instant;

public interface Process {


public enum ProcessCompletionType {
    NotCompleted,
    CompletedNormally,
    TimedOut
}

public long allowableDuration();

public boolean canTimeout();

public long currentDuration();

public String description();

public boolean didProcessingComplete();

public void informTimeout(Instant aTimedOutDate);

public boolean isCompleted();

public boolean isTimedOut();

public boolean notCompleted();

public ProcessCompletionType processCompletionType();

public ProcessId processId();

public Instant startTime();

public TimeConstrainedProcessTracker timeConstrainedProcessTracker();

public Instant timedOutDate();

public long totalAllowableDuration();

public int totalRetriesPermitted();
}
package com.parkingexchange.common.domain.model.process;

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

import java.time.Instant;
public class ProcessTimedOut implements DomainEvent {


private int eventVersion;
private Instant occurredOn;
private ProcessId processId;
private int retryCount;
private int totalRetriesPermitted;
private String moduleName;

public ProcessTimedOut(
        ProcessId aProcessId,
        int aTotalRetriesPermitted,
        int aRetryCount,
        String moduleName) {
    super();

    this.eventVersion = 1;
    this.occurredOn = Instant.now();
    this.processId = aProcessId;
    this.retryCount = aRetryCount;
    this.totalRetriesPermitted = aTotalRetriesPermitted;
    this.moduleName = moduleName;
}

public String moduleName() {
    return moduleName;
}

public boolean allowsRetries() {
    return this.totalRetriesPermitted() > 0;
}

@Override
public int eventVersion() {
    return this.eventVersion;
}

public boolean hasFullyTimedOut() {
    return !this.allowsRetries() || this.totalRetriesReached();
}

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

public ProcessId processId() {
    return processId;
}

public int retryCount() {
    return retryCount;
}

public int totalRetriesPermitted() {
    return totalRetriesPermitted;
}

public boolean totalRetriesReached() {
    return this.retryCount() >= this.totalRetriesPermitted();
}
}
package com.parkingexchange.common.domain.model.process;

import com.parkingexchange.common.domain.model.BaseEntity;
import jakarta.persistence.Entity;

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import com.parkingexchange.common.domain.model.DomainEventPublisher;

import java.lang.reflect.Constructor;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

@Entity
public class TimeConstrainedProcessTracker extends BaseEntity {


@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private long allowableDuration;
private boolean completed;
private String description;
private ProcessId processId;
private boolean processInformedOfTimeout;
private String processTimedOutEventType;
private int retryCount;
private Instant timeoutOccursOn;
private int totalRetriesPermitted;
private String module;

public TimeConstrainedProcessTracker(
        ProcessId aProcessId,
        String aDescription,
        Instant anOriginalStartTime,
        long anAllowableDuration,
        int aTotalRetriesPermitted,
        String aProcessTimedOutEventType,
        String module) {

    super();

    this.setAllowableDuration(anAllowableDuration);
    this.setDescription(aDescription);
    this.setProcessId(aProcessId);
    this.setProcessTimedOutEventType(aProcessTimedOutEventType);
    this.setTimeoutOccursOn(anOriginalStartTime.plus(anAllowableDuration, ChronoUnit.MILLIS));
    this.setTotalRetriesPermitted(aTotalRetriesPermitted);
    this.setModule(module);
}

public void setModule(String module){
    this.module = module;
}

public String getModule(){
    return module;
}

public long allowableDuration() {
    return this.allowableDuration;
}

public void completed() {
    this.completed = true;
}

public boolean isCompleted() {
    return this.completed;
}

public String description() {
    return this.description;
}

public ProcessId processId() {
    return this.processId;
}

public boolean isProcessInformedOfTimeout() {
    return this.processInformedOfTimeout;
}

public String processTimedOutEventType() {
    return this.processTimedOutEventType;
}

public boolean hasTimedOut() {
    Instant now = Instant.now();
    return !timeoutOccursOn.isAfter(now);  // Check if timeout has occurred
}

public void informProcessTimedOut(DomainEventPublisher domainEventPublisher) {
    if (!this.isProcessInformedOfTimeout() && this.hasTimedOut()) {

        ProcessTimedOut processTimedOut = null;

        if (this.totalRetriesPermitted() == 0) {
            processTimedOut = this.processTimedOutEvent();

            this.setProcessInformedOfTimeout(true);
        } else {
            this.incrementRetryCount();

            processTimedOut = this.processTimedOutEventWithRetries();

            if (this.totalRetriesReached()) {
                this.setProcessInformedOfTimeout(true);
            } else {
                this.setTimeoutOccursOn(this.timeoutOccursOn.plus(this.allowableDuration, ChronoUnit.MILLIS));
            }
        }

        domainEventPublisher.publish(processTimedOut);
    }
}

public int retryCount() {
    return this.retryCount;
}

public Instant timeoutOccursOn() {
    return this.timeoutOccursOn;
}

public int totalRetriesPermitted() {
    return this.totalRetriesPermitted;
}

@Override
public boolean equals(Object anObject) {
    boolean equalObjects = false;

    if (anObject != null && this.getClass() == anObject.getClass()) {
        TimeConstrainedProcessTracker typedObject = (TimeConstrainedProcessTracker) anObject;
        equalObjects =
                        this.processId().equals(typedObject.processId());
    }

    return equalObjects;
}

@Override
public int hashCode() {
    int hashCodeValue =
            +(79157 * 107)
                    + this.processId().hashCode();

    return hashCodeValue;
}

@Override
public String toString() {
    return "TimeConstrainedProcessTracker [id = " + id + " allowableDuration=" + allowableDuration + ", completed=" + completed
            + ", description=" + description + ", processId=" + processId + ", processInformedOfTimeout="
            + processInformedOfTimeout + ", processTimedOutEventType=" + processTimedOutEventType + ", retryCount="
            + retryCount + ", timeoutOccursOn=" + timeoutOccursOn + ", totalRetriesPermitted=" + totalRetriesPermitted + "]";
}

protected TimeConstrainedProcessTracker() {
    super();
}

private void incrementRetryCount() {
    this.retryCount++;
}

private void setAllowableDuration(long anAllowableDuration) {
    this.assertArgumentTrue(anAllowableDuration > 0, "The allowable duration must be greater than zero.");

    this.allowableDuration = anAllowableDuration;
}

private void setDescription(String aDescription) {
    this.assertArgumentNotEmpty(aDescription, "Description is required.");
    this.assertArgumentLength(aDescription, 1, 100, "Description must be 1 to 100 characters in length.");

    this.description = aDescription;
}

private void setProcessInformedOfTimeout(boolean isProcessInformedOfTimeout) {
    this.processInformedOfTimeout = isProcessInformedOfTimeout;
}

private ProcessTimedOut processTimedOutEvent() {
    ProcessTimedOut processTimedOut = null;

    try {
        Class<?> processTimedOutClass =
                 Class.forName(this.processTimedOutEventType());

        Constructor<?> ctor = processTimedOutClass.getConstructor(ProcessId.class, String.class);

        processTimedOut = (ProcessTimedOut) ctor.newInstance(this.processId(), this.getModule());

    } catch (Exception e) {
        throw new IllegalStateException(
                "Error creating new ProcessTimedOut instance because: "
                        + e.getMessage());
    }

    return processTimedOut;
}

private ProcessTimedOut processTimedOutEventWithRetries() {
    ProcessTimedOut processTimedOut = null;

    try {
        Class<?> processTimedOutClass =
                Class.forName(this.processTimedOutEventType());

        Constructor<?> ctor =
                processTimedOutClass
                        .getConstructor(
                                ProcessId.class,
                                int.class,
                                int.class,
                                String.class);

        processTimedOut = (ProcessTimedOut)
                ctor.newInstance(
                        this.processId(),
                        this.totalRetriesPermitted(),
                        this.retryCount(),
                        this.getModule());

    } catch (Exception e) {
        throw new IllegalStateException(
                "Error creating new ProcessTimedOut instance because: "
                        + e.getMessage());
    }

    return processTimedOut;
}

private void setProcessId(ProcessId aProcessId) {
    this.assertArgumentNotNull(aProcessId, "ProcessId is required.");

    this.processId = aProcessId;
}

private void setProcessTimedOutEventType(String aProcessTimedOutEventType) {
    this.assertArgumentNotEmpty(aProcessTimedOutEventType, "ProcessTimedOutEventType is required.");

    this.processTimedOutEventType = aProcessTimedOutEventType;
}

private void setTimeoutOccursOn(Instant aTimeoutOccursOn) {
    this.timeoutOccursOn = aTimeoutOccursOn;
}

private void setTotalRetriesPermitted(int aTotalRetriesPermitted) {
    this.assertArgumentTrue(
            aTotalRetriesPermitted >= 0,
            "Total retries must be greater than or equal to zero.");

    this.totalRetriesPermitted = aTotalRetriesPermitted;
}

private boolean totalRetriesReached() {
    return this.retryCount() >= this.totalRetriesPermitted();
}
}package com.parkingexchange.common.domain.model.process;

import java.util.Date;
import java.util.List;
import java.util.Optional;

public interface TimeConstrainedProcessTrackerRepository {


public TimeConstrainedProcessTracker save(TimeConstrainedProcessTracker aTimeConstrainedProcessTracker);

public List<TimeConstrainedProcessTracker> findByCompletedFalseAndProcessInformedOfTimeoutFalseAndTimeoutOccursOnLessThanEqual(Date timeOccursOn);

public List<TimeConstrainedProcessTracker> findAll();

public Optional<TimeConstrainedProcessTracker> findByProcessId(ProcessId aProcessId);
}
package com.parkingexchange.common.domain.model.process;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Optional;

@Service
public class TimeConstrainedProcessTrackerServiceImpl implements TimeConstrainedProcessTrackerService{


@Autowired
public TimeConstrainedProcessTrackerRepository timeConstrainedProcessTrackerRepository;

public TimeConstrainedProcessTrackerServiceImpl(TimeConstrainedProcessTrackerRepository timeConstrainedProcessTrackerRepository){
    this.timeConstrainedProcessTrackerRepository = timeConstrainedProcessTrackerRepository;
}

@Override
public List<TimeConstrainedProcessTracker> allTimedOut(){
    return this.timeConstrainedProcessTrackerRepository.findByCompletedFalseAndProcessInformedOfTimeoutFalseAndTimeoutOccursOnLessThanEqual(new Date());
}

@Override
public List<TimeConstrainedProcessTracker> allTrackers(){
    return this.timeConstrainedProcessTrackerRepository.findAll();
}

@Override
public void save(TimeConstrainedProcessTracker aTimeConstrainedProcessTracker){
    this.timeConstrainedProcessTrackerRepository.save(aTimeConstrainedProcessTracker);
}

@Override
public Optional<TimeConstrainedProcessTracker> trackerOfProcessId(ProcessId aProcessId){
    return this.timeConstrainedProcessTrackerRepository.findByProcessId(aProcessId);
}
}
package com.parkingexchange.common.domain.model.process;

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

public interface TimeConstrainedProcessTrackerService {


public List<TimeConstrainedProcessTracker> allTimedOut();

public List<TimeConstrainedProcessTracker> allTrackers();

public void save(TimeConstrainedProcessTracker aTimeConstrainedProcessTracker);

public Optional<TimeConstrainedProcessTracker> trackerOfProcessId(ProcessId aProcessId);
}

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 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;
import java.util.UUID;

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 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 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.
This method can be called by the leaving user.
It checks for the necessary permissions and updates the status accordingly.
@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 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.
This class encapsulates latitude and longitude coordinates, ensuring they are within valid ranges.
Instances of this class are immutable and can be safely shared. The class uses assertion methods from
{@link AssertionConcern} to validate the input parameters.
*/
@Embeddable
public class Location extends AssertionConcern { private final double latitude;
private final double longitude; /**
Protected no-argument constructor for JPA.
Sets the latitude and longitude to 0.0 by default. This constructor should not be used directly. */ 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.
Two locations are considered equal if they have the same latitude and longitude values.
@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.
This method is supported for the benefit of hash tables such as those provided by
{@link java.util.HashMap}.
@return the hash code value for this location
*/
@Override
public int hashCode() {
return Objects.hash(latitude, longitude);
}
/**
Returns a string representation of this location.
The string representation consists of the latitude and longitude enclosed in parentheses.
@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.
This class is an entity managed by JPA (Java Persistence API), making it persistable in a relational database. */
@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.reward.infrastructure.resporitory;

import com.parkingexchange.reward.domain.model.Availability;
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, UUID> {


@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.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.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);
}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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

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

@Service
@Slf4j
public class RewardApplicationServiceImpl implements RewardApplicationService {


private final RewardRepository rewardRepository;
private final RewardCalculationService rewardCalculationService;
private final RewardProcessingService rewardProcessingService;
private final RewardMapper rewardMapper;

public RewardApplicationServiceImpl(
        RewardRepository rewardRepository,
        RewardCalculationService rewardCalculationService,
        RewardProcessingService rewardProcessingService,
        RewardMapper rewardMapper
) {
    this.rewardRepository = rewardRepository;
    this.rewardCalculationService = rewardCalculationService;
    this.rewardProcessingService = rewardProcessingService;
    this.rewardMapper = rewardMapper;
}

@Transactional
@Override
public RewardResponseDto processReward(UUID availabilityId, UUID fromUserId, UUID toUserId) {
    try {
        // Calculate the reward amount
        BigDecimal amount = rewardCalculationService.calculateRewardAmount(availabilityId);

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

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

        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());
            // Optionally, implement retry logic or notify administrators
        }

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

        return rewardMapper.toDto(reward);

    } catch (Exception e) {
        log.error("Error processing reward for availabilityId: {}", availabilityId, e);
        throw e; // Handle exception appropriately
    }
}
}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 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.application.service;

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

public interface RewardCalculationService {


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

import com.parkingexchange.reward.domain.model.Reward;
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.application.service;

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

public interface RewardProcessingService {


boolean processPayment(Reward reward);
}package com.parkingexchange.reward.domain.model;

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 {


@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();
}

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 {


Optional<Reward> findByRewardId(UUID rewardId);
}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;

@SpringBootApplication
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() {
    }
}

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