content: package io.vividcode.happyride.dispatchservice.domain;
  
  import com.google.common.collect.Lists;
  import io.eventuate.tram.events.aggregates.ResultWithDomainEvents;
  import io.vavr.collection.Stream;
  import io.vividcode.happyride.common.EntityWithGeneratedId;
  import io.vividcode.happyride.common.PositionVO;
  import io.vividcode.happyride.dispatchservice.AvailableDriver;
  import io.vividcode.happyride.dispatchservice.api.events.DispatchDomainEvent;
  import io.vividcode.happyride.dispatchservice.api.events.TripAcceptanceDeclinedEvent;
  import io.vividcode.happyride.dispatchservice.api.events.TripAcceptanceDeclinedReason;
  import io.vividcode.happyride.dispatchservice.api.events.TripAcceptanceSelectedEvent;
  import io.vividcode.happyride.dispatchservice.api.events.TripDispatchFailedEvent;
  import io.vividcode.happyride.dispatchservice.api.events.TripDispatchFailedReason;
  import io.vividcode.happyride.dispatchservice.api.events.TripDispatchedEvent;
  import io.vividcode.happyride.tripservice.api.events.DriverAcceptTripDetails;
  import io.vividcode.happyride.tripservice.api.events.TripDetails;
  import java.math.BigDecimal;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.stream.Collectors;
  import jakarta.persistence.CascadeType;
  import jakarta.persistence.Column;
  import jakarta.persistence.Entity;
  import jakarta.persistence.EnumType;
  import jakarta.persistence.Enumerated;
  import jakarta.persistence.FetchType;
  import jakarta.persistence.JoinColumn;
  import jakarta.persistence.OneToMany;
  import jakarta.persistence.Table;
  import jakarta.validation.constraints.Size;
  import lombok.Getter;
  import lombok.NoArgsConstructor;
  import lombok.NonNull;
  import lombok.RequiredArgsConstructor;
  import lombok.Setter;
  import lombok.ToString;
  
  @Entity
  @Table(name = "dispatches")
  @NoArgsConstructor
  @RequiredArgsConstructor
  @Getter
  @Setter
  @ToString
  public class Dispatch extends EntityWithGeneratedId {
  
    @NonNull
    @Column(name = "trip_id")
    @Size(max = 36)
    private String tripId;
  
    @NonNull
    @Column(name = "start_pos_lng")
    private BigDecimal startPosLng;
  
    @NonNull
    @Column(name = "start_pos_lat")
    private BigDecimal startPosLat;
  
    @NonNull
    @Enumerated(EnumType.STRING)
    @Column(name = "state")
    private DispatchState state = DispatchState.WAIT_FOR_ACCEPTANCE;
  
    @Enumerated(EnumType.STRING)
    @Column(name = "failed_reason")
    private TripDispatchFailedReason failedReason;
  
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @JoinColumn(name = "dispatch_id", referencedColumnName = "id", nullable = false)
    private List<TripAcceptance> tripAcceptances = Lists.newArrayList();
  
    public static ResultWithDomainEvents<Dispatch, DispatchDomainEvent> createDispatch(
        final String tripId,
        final TripDetails tripDetails,
        final Set<AvailableDriver> drivers) {
      final PositionVO startPos = tripDetails.getStartPos();
      final Dispatch dispatch = new Dispatch(tripId, startPos.getLng(),
          startPos.getLat());
      final List<TripAcceptance> tripAcceptances = drivers.stream()
          .map(driver -> new TripAcceptance(driver.getDriverId(),
              driver.getPosLng(),
              driver.getPosLat()))
          .collect(Collectors.toList());
      dispatch.setTripAcceptances(tripAcceptances);
      final DispatchDomainEvent event;
      if (drivers.isEmpty()) {
        dispatch.setState(DispatchState.FAILED);
        dispatch.setFailedReason(TripDispatchFailedReason.NO_DRIVERS_AVAILABLE);
        event = new TripDispatchFailedEvent(tripId,
            TripDispatchFailedReason.NO_DRIVERS_AVAILABLE);
      } else {
        final Set<String> driversId = drivers.stream()
            .map(AvailableDriver::getDriverId)
            .collect(Collectors.toSet());
        event = new TripDispatchedEvent(tripId, tripDetails, driversId);
      }
      return new ResultWithDomainEvents<>(dispatch, event);
    }
  
    public Dispatch submitTripAcceptance(
        final DriverAcceptTripDetails acceptTripDetails) {
      Stream.ofAll(this.tripAcceptances)
          .find(tripAcceptance -> Objects
              .equals(tripAcceptance.getDriverId(),
                  acceptTripDetails.getDriverId()))
          .toJavaOptional()
          .ifPresent(tripAcceptance -> {
            tripAcceptance.setState(TripAcceptanceState.SUBMITTED);
            tripAcceptance.setCurrentPosLng(acceptTripDetails.getPosLng());
            tripAcceptance.setCurrentPosLat(acceptTripDetails.getPosLat());
          });
      return this;
    }
  
    public ResultWithDomainEvents<Dispatch, DispatchDomainEvent> selectTripAcceptance(
        final String driverId) {
      this.setState(DispatchState.ACCEPTANCE_SELECTED);
      final Map<Boolean, List<TripAcceptance>> acceptances = this.tripAcceptances
          .stream()
          .collect(Collectors.groupingBy(tripAcceptance -> Objects
              .equals(tripAcceptance.getDriverId(), driverId)));
      final List<TripAcceptance> toAccept = acceptances
          .getOrDefault(Boolean.TRUE, Collections.emptyList());
      toAccept.forEach(
          acceptance -> acceptance.setState(TripAcceptanceState.SELECTED));
      final List<TripAcceptance> toDecline = acceptances
          .getOrDefault(Boolean.FALSE, Collections.emptyList());
      toDecline.forEach(
          acceptance -> acceptance.setState(TripAcceptanceState.DECLINED));
      final List<DispatchDomainEvent> events = Lists.newArrayList();
      events.addAll(toAccept.stream()
          .map(acceptance -> new TripAcceptanceSelectedEvent(this.tripId,
              acceptance.getDriverId()))
          .collect(Collectors.toList()));
      events.addAll(toDecline.stream()
          .map(acceptance -> new TripAcceptanceDeclinedEvent(this.tripId,
              acceptance.getDriverId(),
              TripAcceptanceDeclinedReason.OTHER_SELECTED))
          .collect(Collectors.toList()));
      return new ResultWithDomainEvents<>(this,
          events.toArray(new DispatchDomainEvent[0]));
    }
  
    public ResultWithDomainEvents<Dispatch, DispatchDomainEvent> markAsFailed(
        final String tripId,
        final TripDispatchFailedReason reason) {
      this.setState(DispatchState.FAILED);
      this.setFailedReason(reason);
      final TripDispatchFailedEvent event = new TripDispatchFailedEvent(tripId,
          reason);
      return new ResultWithDomainEvents<>(this, event);
    }
  
    public ResultWithDomainEvents<Dispatch, DispatchDomainEvent> markAsCancelled() {
      this.setState(DispatchState.CANCELLED);
      this.tripAcceptances.forEach(tripAcceptance -> {
        tripAcceptance.setState(TripAcceptanceState.DECLINED);
        tripAcceptance
            .setDeclinedReason(TripAcceptanceDeclinedReason.TRIP_CANCELLED);
      });
      return new ResultWithDomainEvents<>(this);
    }
  }
  