do you think this in the core part of my application can be used to do saga?
package 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);
}


Thank you for sharing the code from the core part of your application. You've provided an implementation of an AbstractProcess and related classes that seem to handle process tracking, timeouts, and retries. Your question is whether this code can be used to implement the Saga pattern in your application.

Short Answer
Yes, the code you've provided can serve as a foundational component for implementing the Saga pattern, particularly in orchestrating long-running processes with time constraints, retries, and process tracking. It appears to provide a framework for managing process states, handling timeouts, and publishing domain events when processes time out or complete. With some adaptation and extension, you can leverage this code to orchestrate sagas in your application.

Detailed Explanation
Understanding the Provided Code
Let's break down the key components of your code to understand how they relate to implementing a Saga:

AbstractProcess Class
Purpose: Serves as a base class for processes that need to be tracked over time, may have allowable durations (timeouts), and can be retried.
Key Features:
Process Identification: Uses a ProcessId to uniquely identify each process.
Completion Types: Handles different ProcessCompletionType values (NotCompleted, CompletedNormally, TimedOut).
Timeout Handling: Supports timeouts through allowableDuration and timedOutDate.
Retries: Tracks the number of retries and the total retries permitted.
Event Publication: The process can inform listeners when it times out via informTimeout.
TimeConstrainedProcessTracker Class
Purpose: Tracks the state of time-constrained processes, determining if they have timed out and managing retries.
Key Features:
Timeout Detection: Checks if the process has timed out and informs the process accordingly.
Retry Logic: Increments retry counts and adjusts the timeoutOccursOn time as needed.
Event Creation: Generates ProcessTimedOut domain events when processes time out.
ProcessTimedOut Event
Purpose: Represents a domain event that is published when a process has timed out.
Contains:
Process ID: Identifies the timed-out process.
Retry Information: Indicates how many retries have occurred and the total allowed retries.
Module Name: Specifies the module the process belongs to.
ProcessId Class
Purpose: Represents a unique identifier for processes.
Service and Repository Interfaces
TimeConstrainedProcessTrackerService and TimeConstrainedProcessTrackerRepository: Provide methods to manage TimeConstrainedProcessTracker instances.
Relation to the Saga Pattern
The Saga pattern, especially the orchestration-based saga, requires components that can:

Coordinate Multiple Steps: Orchestrate multiple operations across different services or components.
Maintain State: Keep track of the current state of the saga and its progress.
Handle Compensations: Execute compensating actions if part of the process fails.
Manage Timeouts and Retries: Handle cases where steps may fail temporarily and need to be retried.
Your code aligns with these requirements in the following ways:

Process Coordination and Orchestration
The AbstractProcess class can serve as an orchestrator that manages the execution of a saga.
It can define the steps required in the saga and manage the flow between them.
State Management
The TimeConstrainedProcessTracker keeps track of the process state, including retries and timeouts.
Persistent tracking allows the saga to resume or compensate after failures or restarts.
Timeouts and Retries
The built-in support for allowable durations and retries in AbstractProcess and TimeConstrainedProcessTracker is essential for long-running transactions and ensuring reliability.
Event-Driven Communication
The ProcessTimedOut domain event supports an event-driven architecture, which is common in saga implementations.
Publishing events upon timeouts or other state changes allows other components or services to react accordingly.
Using the Code for Implementing a Saga
Here's how you can adapt and use your code to implement the Saga pattern in your application:

Define Specific Process Subclasses
Create concrete subclasses of AbstractProcess for each specific saga you need to orchestrate. For example, SpotConfirmationSaga or RewardProcessingSaga.
Implement the completenessVerified() and processTimedOutEventType() methods to define when the saga is considered complete and which event to publish upon timeout.
Implement Saga Steps
Within your concrete process class, define methods to execute each step of the saga.
Steps may involve synchronous calls to other services or publishing events and awaiting responses.
Manage Saga State
Use the TimeConstrainedProcessTracker to persist the state of each saga instance.
Update the tracker as the saga progresses through its steps.
Handle Timeouts and Retries
Configure allowableDuration and totalRetriesPermitted to control how long the saga should run and how many times it should retry failed steps.
Use the informTimeout() method to handle cases where the saga exceeds its allowable duration.
Compensating Transactions
Define compensating actions within your saga steps to undo any side effects in case of failures.
Use domain events to trigger these compensations in other services.
Event Handling and Publishing
Leverage the domain event publisher to emit events when significant state changes occur (e.g., process completion, timeouts).
Ensure that event listeners in other services react appropriately to these events.
Integrating with Your Application
To integrate this process management into your application:

Parking Availability Service as Orchestrator
Use a subclass of AbstractProcess to manage the confirmation and reward processing as a saga.
For example, create a SpotTakenConfirmationProcess that coordinates the update of the parking spot status and the reward processing.
Define Process Steps
Step 1: Update the parking spot status in the Parking Availability Service.
Step 2: Invoke the Reward Service to process the reward.
Step 3: (Optional) Notify other services or update additional state.
Handle Failures and Compensations
If any step fails, use the compensating actions to roll back previous steps.
For example, if the reward processing fails after updating the parking spot status, you might need to reverse the parking spot update or flag the record for manual review.
Timeouts and Retries
Configure timeouts to handle cases where services are unresponsive.
Implement retries for transient failures.
Benefits of Using Your Code
Separation of Concerns
Centralizes saga orchestration logic within the process classes.
Keeps domain logic organized and maintainable.
Reusability
The abstract classes and interfaces provide a reusable framework for different sagas in your application.
Alignment with DDD Principles
By encapsulating saga logic within domain models and using domain events, you adhere to Domain-Driven Design best practices.
The code enforces invariants and business rules within the domain layer.
Considerations and Recommendations
While your code provides a solid foundation, there are some considerations and potential enhancements:

Process State Persistence
Ensure that your TimeConstrainedProcessTracker and process instances are persisted reliably.
Consider using a robust persistence mechanism (e.g., a relational database with transaction support).
Concurrency Handling
Be cautious of concurrent modifications to the same process instance.
Implement appropriate locking or concurrency control mechanisms.
Distributed Transactions
Recognize that despite orchestrating the saga, you cannot achieve strict ACID transactions across microservices.
Design your saga steps to be idempotent and support eventual consistency.
Compensating Actions Complexity
Carefully design compensating actions to handle partial failures without causing further inconsistencies.
Test compensations thoroughly to ensure they behave correctly in various failure scenarios.
Monitoring and Logging
Implement comprehensive logging within your process classes to trace the flow of sagas.
Use monitoring tools to observe the health and performance of your sagas.
Error Handling and Recovery
Ensure that exceptions and errors within saga steps are handled gracefully.
Implement retry policies where appropriate, and avoid infinite retry loops.
Scalability
Evaluate how your saga orchestration scales with increased load.
Consider potential bottlenecks, such as a single orchestrator handling all sagas.
Alternative Approaches
If you find that implementing sagas using your code introduces significant complexity, you might consider:

Using a Saga Management Library or Framework
Libraries like Axon Framework or Eventuate Tram Sagas provide built-in support for sagas, including state management, compensation, and transaction coordination.
These can reduce the amount of custom code you need to write and maintain.
Implementing Choreography-Based Sagas
If appropriate for your use case, you might consider using a choreography-based saga where services react to events without a central orchestrator.
This approach reduces the need for centralized process tracking but may increase the complexity of managing the overall saga flow.
Example Usage in Your Context
Here's how you might start implementing a saga using your code:

Create a Concrete Process Class

public class SpotConfirmationProcess extends AbstractProcess {

    public SpotConfirmationProcess(ProcessId processId, String description, long allowableDuration, String module) {
        super(processId, description, allowableDuration, module);
    }

    @Override
    protected boolean completenessVerified() {
        // Check if both the parking spot update and reward processing are completed
        // This might involve checking the state of local variables or database records
        return parkingSpotUpdated && rewardProcessed;
    }

    @Override
    protected Class<? extends ProcessTimedOut> processTimedOutEventType() {
        return SpotConfirmationProcessTimedOut.class;
    }

    // Define steps for the saga
    public void confirmSpotTaken(UUID availabilityId, UUID arrivingUserId, UUID leavingUserId) {
        // Step 1: Update parking spot status
        // Handle potential exceptions and update process state

        // Step 2: Call Reward Service
        // Handle potential exceptions and update process state
    }
}
Implement the ProcessTimedOut Subclass

public class SpotConfirmationProcessTimedOut extends ProcessTimedOut {
    public SpotConfirmationProcessTimedOut(ProcessId processId, String module) {
        super(processId, module);
        // Additional initialization if needed
    }
}
Use the Process in Your Service
Instantiate the SpotConfirmationProcess when the user confirms the spot.
Execute the saga steps within the process.
Persist the process state using the TimeConstrainedProcessTracker.
Handle Timeouts and Retries
If the process times out, use the ProcessTimedOut event to trigger compensating actions.
Conclusion
In summary, your existing code provides a strong foundation for implementing the Saga pattern in your application. By extending and adapting it to orchestrate your specific sagas, you can manage distributed transactions across your microservices while adhering to Domain-Driven Design principles.

Key Actions:

Define Concrete Process Classes: Tailor the abstract process to your specific saga needs.
Implement Saga Steps: Clearly define each step and handle success and failure cases.
Manage State and Persistence: Ensure process states are persisted and recoverable.
Handle Compensations: Design compensating actions for failure scenarios.
Test Thoroughly: Validate your saga implementations under various conditions, including failures and retries.
By carefully implementing these components, you can create a robust saga mechanism that enhances consistency and reliability in your application, ultimately providing a better user experience.

If you have further questions or need assistance with specific aspects of implementing sagas using your code, please let me know, and I'd be happy to help!


package com.parkingexchange.reward.application.dto;


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

public class AvailabilityRequest {

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


    // Getters and Setters

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

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

    public double getLatitude() {
        return latitude;
    }

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

    public double getLongitude() {
        return longitude;
    }

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

    public LocalDateTime getEstimatedLeavingTime() {
        return estimatedLeavingTime;
    }

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

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


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

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

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

    // Getters and setters


    // Getters and Setters

    public UUID getAvailabilityId() {
        return availabilityId;
    }

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

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

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

    public double getLatitude() {
        return latitude;
    }

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

    public double getLongitude() {
        return longitude;
    }

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

    public LocalDateTime getEstimatedLeavingTime() {
        return estimatedLeavingTime;
    }

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

    public AvailabilityStatus getStatus() {
        return status;
    }

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

    public UUID getReservedByUserId() {
        return reservedByUserId;
    }

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

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

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

import java.util.UUID;

public class CancelAvailabilityRequest {

    private UUID userId;

    // Getters and Setters

    public UUID getUserId() {
        return userId;
    }

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

import java.util.UUID;

public class ConfirmSpotTakenRequest {

    private UUID arrivingUserId;

    // Getters and Setters

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

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

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

import java.util.UUID;

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

    AvailabilityResponse toResponse(Availability availability);

    ReservationRequestResponse toReservationResponse(ReservationRequest request);

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

import java.util.UUID;

public class ReservationRequestDto {

    private UUID arrivingUserId;
    private String message;

    // Getters and Setters

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

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

    public String getMessage() {
        return message;
    }

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

import java.util.UUID;


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

public class ReservationRequestResponse {

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

    // Getters and Setters

    public UUID getRequestId() {
        return requestId;
    }

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

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

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

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

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

    public RequestStatus getStatus() {
        return status;
    }

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

    public LocalDateTime getRequestedAt() {
        return requestedAt;
    }

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

    public LocalDateTime getRespondedAt() {
        return respondedAt;
    }

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

    public String getMessage() {
        return message;
    }

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

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

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

public class ReservationResponseDto {

    private UUID leavingUserId;
    private boolean accepted;

    // Getters and Setters

    public UUID getLeavingUserId() {
        return leavingUserId;
    }

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

    public boolean isAccepted() {
        return accepted;
    }

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

public class SearchCriteria {

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

    // Getters and Setters

    public double getLatitude() {
        return latitude;
    }

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

    public double getLongitude() {
        return longitude;
    }

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

    public double getRadius() {
        return radius;
    }

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



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


@Service
public class AvailabilityApplicationServiceImpl implements AvailabilityApplicationService {

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

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

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

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

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

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

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

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

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

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

        publishDomainEvents(availability);

        return availabilityMapper.toReservationResponse(reservationRequest);

    }

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

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

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

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

        publishDomainEvents(availability);

        return availabilityMapper.toReservationResponse(reservationRequest);
    }

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

        availability.confirmTaken(request.getArrivingUserId());

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

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

        availability.cancel(request.getUserId());

        availabilityRepository.save(availability);

        publishDomainEvents(availability);

        return availabilityMapper.toResponse(availability);
    }

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


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

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

public interface AvailabilityApplicationService {

    AvailabilityResponse reportAvailability(AvailabilityRequest request);

    List<AvailabilityResponse> searchAvailableSpots(SearchCriteria criteria);

    ReservationRequestResponse requestReservation(AvailabilityId availabilityId, ReservationRequestDto requestDto);

    ReservationRequestResponse respondToReservation(UUID requestId, ReservationResponseDto responseDto);

    AvailabilityResponse confirmSpotTaken(AvailabilityId availabilityId, ConfirmSpotTakenRequest request);

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

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

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

public class AvailabilityCanceledEvent implements DomainEvent {

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


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

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

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

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

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

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

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

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

public class ReservationRequestCreatedEvent implements DomainEvent {

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

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

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

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

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

}

package com.parkingexchange.reward.domain.event;

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

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

public class ReservationRequestRespondedEvent implements DomainEvent {

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


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

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

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

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

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getRequestId() {
        return requestId;
    }

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

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

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

public class SpotTakenConfirmedEvent implements DomainEvent {

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


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

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

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

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

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public UUID getArrivingUserId() {
        return arrivingUserId;
    }

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


import jakarta.persistence.Embeddable;

import java.util.UUID;

@Embeddable
public final class AvailabilityId {

    private final UUID id;

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

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

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

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

    public UUID getValue() {
        return id;
    }

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

        AvailabilityId that = (AvailabilityId) o;

        return id.equals(that.id);
    }

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

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

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

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


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

    @EmbeddedId
    private AvailabilityId availabilityId;

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

    @NotNull
    @Valid
    @Embedded
    private Location location;

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

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

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

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

    protected Availability() {
        // Required by JPA
    }

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

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

    // Business methods

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

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

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

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

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

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

        return request;
    }

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

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

        request.respond(leavingUserId, accepted);

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

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

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

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

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

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

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

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

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

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

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

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

    // Getters and Setters with validation where applicable

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

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

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

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

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

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

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

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

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

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

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

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

    // equals and hashCode based on 'availabilityId'

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

        Availability that = (Availability) o;

        return availabilityId.equals(that.availabilityId);
    }

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

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

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


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

import java.util.Objects;

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

    private final double latitude;
    private final double longitude;

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

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

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

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

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

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

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

        Location location = (Location) o;

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

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

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

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

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

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

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

@Entity
public class ReservationRequest extends AssertionConcern {

    @Id
    private UUID requestId;

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

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

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

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

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

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

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

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

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

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

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

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

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

    // Getters and Setters with validation

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public interface AvailabilityRepository {

    Availability save(Availability availability);

    Optional<Availability> findByAvailabilityId(AvailabilityId availabilityId);

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

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

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

@Configuration
public class ModelMapperConfig {

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

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

@Configuration
public class WebClientConfig {

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

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

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

    private final AvailabilityApplicationService availabilityService;

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

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

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

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

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

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

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

public class UserExistsResponse {

    private boolean exists;

    // Getters and Setters

    public boolean isExists() {
        return exists;
    }

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


public class UserReputationResponse {

    private double reputation;

    // Getters and Setters

    public double getReputation() {
        return reputation;
    }

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

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

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

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

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

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


}package com.parkingexchange.reward;

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

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

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

}
package com.parkingexchange.reward;

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

@SpringBootTest
class ParkingApplicationTests {

	@Test
	void contextLoads() {
	}

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

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

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

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

        AvailabilityResponse availabilityResponse = new AvailabilityResponse();

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

        return availabilityResponse;
    }

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

        ReservationRequestResponse reservationRequestResponse = new ReservationRequestResponse();

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

        return reservationRequestResponse;
    }

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

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

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

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

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

public class RewardResponseDto {

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

    // Getters and Setters

    public UUID getRewardId() {
        return rewardId;
    }

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

    public UUID getFromUserId() {
        return fromUserId;
    }

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

    public UUID getToUserId() {
        return toUserId;
    }

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

    public UUID getAvailabilityId() {
        return availabilityId;
    }

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

    public BigDecimal getAmount() {
        return amount;
    }

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

    public RewardStatus getStatus() {
        return status;
    }

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

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

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

    public LocalDateTime getCompletedAt() {
        return completedAt;
    }

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


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

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

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

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

import org.springframework.transaction.annotation.Transactional;

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

@Service
public class RewardApplicationServiceImpl implements RewardApplicationService {

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

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

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

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

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

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

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

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

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

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


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

import java.util.UUID;

public interface RewardApplicationService {

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

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

@Service
@Slf4j
public class RewardProcessingServiceImpl implements RewardProcessingService {

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

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

            // Payment successful
            return true;

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

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

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

public class RewardCompletedEvent implements DomainEvent {

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

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

    public UUID getRewardId() {
        return rewardId;
    }

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

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

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


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

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

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

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

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

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

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

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

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

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

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

    protected Reward() {
        // For JPA
    }

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

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

    // Business Methods

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

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

    // Getters (no setters for immutability)

    public UUID getRewardId() {
        return rewardId;
    }

    public UUID getFromUserId() {
        return fromUserId;
    }

    public UUID getToUserId() {
        return toUserId;
    }

    public UUID getAvailabilityId() {
        return availabilityId;
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public RewardStatus getStatus() {
        return status;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public LocalDateTime getCompletedAt() {
        return completedAt;
    }

    // Override equals and hashCode based on rewardId

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

        Reward reward = (Reward) o;

        return rewardId.equals(reward.rewardId);
    }

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

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

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

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

public interface RewardRepository {

    Reward save(Reward reward);

    Optional<Reward> findByRewardId(UUID rewardId);

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

import org.springframework.stereotype.Service;

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

@Service
public class RewardCalculationServiceImpl implements RewardCalculationService {

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

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

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

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

public interface RewardCalculationService {

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

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

public interface RewardProcessingService {

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

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

import java.util.UUID;

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

    private final RewardApplicationService rewardApplicationService;

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

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

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

import java.util.UUID;

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


}package com.parkingexchange.reward;

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

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

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

}
package com.parkingexchange.reward;

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

@SpringBootTest
class RewardApplicationTests {

	@Test
	void contextLoads() {
	}

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

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

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

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

        RewardResponseDto rewardResponseDto = new RewardResponseDto();

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

        return rewardResponseDto;
    }
}