Covivo/mobicoop

View on GitHub
api/src/Carpool/Service/DynamicManager.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

/**
 * Copyright (c) 2020, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 */

namespace App\Carpool\Service;

use App\Carpool\Entity\Ask;
use App\Carpool\Entity\AskHistory;
use App\Carpool\Entity\CarpoolProof;
use App\Carpool\Entity\Criteria;
use App\Carpool\Entity\Matching;
use App\Carpool\Entity\Position;
use App\Carpool\Entity\Proposal;
use App\Carpool\Entity\Result;
use App\Carpool\Entity\Waypoint;
use App\Carpool\Exception\DynamicException;
use App\Carpool\Exception\ProofException;
use App\Carpool\Repository\AskHistoryRepository;
use App\Carpool\Repository\AskRepository;
use App\Carpool\Repository\MatchingRepository;
use App\Carpool\Ressource\Dynamic;
use App\Carpool\Ressource\DynamicAsk;
use App\Carpool\Ressource\DynamicProof;
use App\Communication\Entity\Message;
use App\Communication\Entity\Recipient;
use App\Communication\Service\InternalMessageManager;
use App\Geography\Entity\Address;
use App\Geography\Entity\Direction;
use App\Geography\Service\AddressCompleter;
use App\Geography\Service\Geocoder\GeocoderFactory;
use App\Geography\Service\GeoRouter;
use App\Geography\Service\GeoTools;
use App\Geography\Service\Point\AddressAdapter;
use App\Geography\Service\Point\GeocoderPointProvider;
use App\User\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * Dynamic ad manager service.
 *
 * @author Sylvain Briat <sylvain.briat@mobicoop.org>
 */
class DynamicManager
{
    private $entityManager;
    private $proposalManager;
    private $proposalMatcher;
    private $askManager;
    private $resultManager;
    private $geoTools;
    private $geoRouter;
    private $reversePointProvider;
    private $addressCompleter;
    private $params;
    private $logger;
    private $matchingRepository;
    private $askRespository;
    private $askHistoryRepository;
    private $internalMessageManager;
    private $proofManager;

    /**
     * Constructor.
     */
    public function __construct(
        EntityManagerInterface $entityManager,
        ProposalManager $proposalManager,
        ProposalMatcher $proposalMatcher,
        AskManager $askManager,
        ResultManager $resultManager,
        GeoTools $geoTools,
        GeoRouter $geoRouter,
        GeocoderFactory $geocoderFactory,
        array $params,
        LoggerInterface $logger,
        MatchingRepository $matchingRepository,
        AskRepository $askRespository,
        AskHistoryRepository $askHistoryRepository,
        InternalMessageManager $internalMessageManager,
        ProofManager $proofManager
    ) {
        $this->entityManager = $entityManager;
        $this->proposalManager = $proposalManager;
        $this->proposalMatcher = $proposalMatcher;
        $this->askManager = $askManager;
        $this->resultManager = $resultManager;
        $this->geoTools = $geoTools;
        $this->geoRouter = $geoRouter;
        $this->params = $params;
        $this->logger = $logger;
        $this->matchingRepository = $matchingRepository;
        $this->askRespository = $askRespository;
        $this->askHistoryRepository = $askHistoryRepository;
        $this->internalMessageManager = $internalMessageManager;
        $this->proofManager = $proofManager;

        $this->reversePointProvider = new GeocoderPointProvider($geocoderFactory->getGeocoder());
        $this->addressCompleter = new AddressCompleter($this->reversePointProvider);
    }

    // DYNAMIC AD

    /**
     * Get a dynamic ad.
     *
     * @param int $id the dynamic ad id
     *
     * @return Dynamic the dynamic ad
     */
    public function getDynamic(int $id)
    {
        if (!$proposal = $this->proposalManager->get($id)) {
            throw new DynamicException('Dynamic ad not found');
        }
        $dynamic = new Dynamic();
        $dynamic->setProposal($proposal);
        $dynamic->setUser($proposal->getUser());
        $dynamic->setRole($proposal->getCriteria()->isDriver() ? Dynamic::ROLE_DRIVER : Dynamic::ROLE_PASSENGER);
        $dynamic->setId($proposal->getId());

        return $dynamic;
    }

    /**
     * Create a dynamic ad.
     *
     * @param Dynamic $dynamic The dynamic ad to create
     *
     * @return Dynamic the created Dynamic ad
     */
    public function createDynamic(Dynamic $dynamic)
    {
        // first we check if the user has already a dynamic ad pending
        if ($this->proposalManager->hasPendingDynamic($dynamic->getUser())) {
            throw new DynamicException('This user has already a pending dynamic ad');
        }

        // set Seats
        if (is_null($dynamic->getSeats())) {
            if (Dynamic::ROLE_DRIVER == $dynamic->getRole()) {
                $dynamic->setSeats($this->params['defaultSeatsDriver']);
            } else {
                $dynamic->setSeats($this->params['defaultSeatsPassenger']);
            }
        }
        // set Date
        if (is_null($dynamic->getDate())) {
            $dynamic->setDate(new \DateTime($this->params['dynamicTimezone']));
        }

        // creation of the proposal
        $this->logger->info('DynamicManager : start '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        $proposal = new Proposal();
        $criteria = new Criteria();
        $position = new Position();
        $direction = new Direction();

        $proposal->setUser($dynamic->getUser());

        // special dynamic properties
        $proposal->setDynamic(true);
        $proposal->setActive(true);
        $proposal->setFinished(false);
        $proposal->setType(Proposal::TYPE_ONE_WAY);

        // comment
        $proposal->setComment($dynamic->getComment());

        // criteria

        // driver / passenger / seats
        $criteria->setDriver(Dynamic::ROLE_DRIVER == $dynamic->getRole());
        $criteria->setPassenger(Dynamic::ROLE_PASSENGER == $dynamic->getRole());
        $criteria->setSeatsDriver(Dynamic::ROLE_DRIVER == $dynamic->getRole() ? $dynamic->getSeats() : 0);
        $criteria->setSeatsPassenger(Dynamic::ROLE_PASSENGER == $dynamic->getRole() ? $dynamic->getSeats() : 0);

        // prices
        $criteria->setPriceKm($dynamic->getPriceKm());
        if (Dynamic::ROLE_DRIVER == $dynamic->getRole()) {
            $criteria->setDriverPrice($dynamic->getPrice());
        }
        if (Dynamic::ROLE_PASSENGER == $dynamic->getRole()) {
            $criteria->setPassengerPrice($dynamic->getPrice());
        }

        // dates and times

        // we use the current date
        $criteria->setFromDate($dynamic->getDate());
        $criteria->setFromTime($dynamic->getDate());
        $criteria->setFrequency(Criteria::FREQUENCY_PUNCTUAL);

        // waypoints
        foreach ($dynamic->getWaypoints() as $waypointPosition => $point) {
            $waypoint = new Waypoint();
            $address = $this->addressCompleter->getAddressByPartialAddressArray($point);
            $waypoint->setAddress($address);
            $waypoint->setPosition($waypointPosition);
            $waypoint->setDestination($waypointPosition == count($dynamic->getWaypoints()) - 1);
            $proposal->addWaypoint($waypoint);

            if (0 == $waypointPosition) {
                // init position => the origin of the proposal
                // we double this waypoint : it will be a floating waypoint that will reflect the current position of the user (useful for matching)
                // the position of this waypoint will always be 0
                $floatingWaypoint = clone $waypoint;
                $floatingWaypoint->setFloating(true);
                $proposal->addWaypoint($floatingWaypoint);
                $position->setWaypoint($floatingWaypoint);
                $position->setPoints([$address]);
                $waypoint->setReached(true);

                // direction
                $direction->setPoints([$address]);
                $direction->setDistance(0);
                $direction->setDuration(0);
                $direction->setDetail('');
                $direction->setSnapped('');
                $direction->setFormat('Dynamic');
                $position->setDirection($direction);
            }
        }

        $proposal->setCriteria($criteria);

        $proposal = $this->proposalManager->prepareProposal($proposal);
        $this->logger->info('DynamicManager : end creating ad '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        $this->entityManager->persist($proposal);
        $this->logger->info('DynamicManager : end persisting ad '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        $position->setProposal($proposal);
        $this->entityManager->persist($position);
        $this->entityManager->flush();

        if (Dynamic::ROLE_DRIVER == $dynamic->getRole()) {
            $dynamic->setPrice($proposal->getCriteria()->getDriverComputedRoundedPrice());
        } else {
            $dynamic->setPrice($proposal->getCriteria()->getPassengerComputedRoundedPrice());
        }

        // we compute the results

        // default order
        $dynamic->setFilters([
            'order' => [
                'criteria' => 'date',
                'value' => 'ASC',
            ],
        ]);

        $dynamic->setResults(
            $this->resultManager->orderResults(
                $this->resultManager->filterResults(
                    $this->resultManager->createAdResults($proposal),
                    $dynamic->getFilters()
                ),
                $dynamic->getFilters()
            )
        );

        $dynamic->setId($proposal->getId());

        return $dynamic;
    }

    /**
     * Update a dynamic ad => update the current position.
     *
     * @param int     $id          The id of the dynamic ad to update
     * @param Dynamic $dynamicData The dynamic ad data to make the update
     *
     * @return Dynamic the updated Dynamic ad
     */
    public function updateDynamic(int $id, Dynamic $dynamicData)
    {
        // we get the original dynamic ad
        $dynamic = $this->getDynamic($id);

        // dynamic ad ?
        if (!$dynamic->getProposal()->isDynamic()) {
            throw new DynamicException('This ad is not dynamic !');
        }

        // not already finished ?
        if ($dynamic->getProposal()->isFinished()) {
            throw new DynamicException('This ad is finished !');
        }

        // the user indicates that the ad is finished
        if ($dynamicData->isFinished()) {
            $dynamic->getProposal()->setFinished(true);
            $dynamic->getProposal()->setActive(false);
            $dynamic->setFinished(true);
            // persist the updates
            $this->entityManager->persist($dynamic->getProposal());
            $this->entityManager->flush();

            return $dynamic;
        }

        // last point check
        if ($this->params['dynamicEnableMaxSpeed']) {
            // we compute the direction between the 2 last points to get the average speed
            // => we exclude the point if the speed is too high (can happen with bad GPS coordinates, eg. bad lane guessing on motorways)
            $now = new \DateTime($this->params['dynamicTimezone']);
            $newAddress = new Address();
            $newAddress->setLongitude($dynamicData->getLongitude());
            $newAddress->setLatitude($dynamicData->getLatitude());
            $addresses = [
                $dynamic->getProposal()->getPosition()->getWaypoint()->getAddress(),
                $newAddress,
            ];
            if ($routes = $this->geoRouter->getRoutes($addresses)) {
                // we have a direction
                $distance = $routes[0]->getDistance();
                $interval = $now->diff($dynamic->getProposal()->getPosition()->getUpdatedDate());
                $seconds = ((($interval->format('%a') * 24) + $interval->format('%H')) * 60 + $interval->format('%i')) * 60 + $interval->format('%s');
                if (($distance / $seconds) > $this->params['dynamicMaxSpeed']) {
                    throw new DynamicException('Speed too high since the last point ('.round($distance / $seconds * 3.6).' kmh) ignoring last point');
                }
            }
        }

        // we update the position
        $dynamic->setLatitude($dynamicData->getLatitude());
        $dynamic->setLongitude($dynamicData->getLongitude());

        // update the address geographic coordinates
        $dynamic->getProposal()->getPosition()->getWaypoint()->getAddress()->setLongitude($dynamic->getLongitude());
        $dynamic->getProposal()->getPosition()->getWaypoint()->getAddress()->setLatitude($dynamic->getLatitude());

        // we search if we have reached a waypoint
        foreach ($dynamic->getProposal()->getWaypoints() as $waypoint) {
            /**
             * @var Waypoint $waypoint
             */
            if (!$waypoint->isReached() && !$waypoint->isFloating()) {
                if ($this->geoTools->haversineGreatCircleDistance($dynamic->getLatitude(), $dynamic->getLongitude(), $waypoint->getAddress()->getLatitude(), $waypoint->getAddress()->getLongitude()) < $this->params['dynamicReachedDistance']) {
                    $waypoint->setReached(true);
                    $this->entityManager->persist($waypoint);
                    $this->entityManager->flush();
                }
            }
            if ($waypoint->isDestination() && $waypoint->isReached() && $this->geoTools->haversineGreatCircleDistance($dynamic->getLatitude(), $dynamic->getLongitude(), $waypoint->getAddress()->getLatitude(), $waypoint->getAddress()->getLongitude()) < $this->params['dynamicDestinationDistance']) {
                $dynamic->setDestination(true);
            }
            if ($waypoint->isFloating()) {
                // update the floating waypoint address
                // we reverse geocode, to get a full address
                if ($points = $this->reversePointProvider->reverse((float) $dynamic->getLongitude(), (float) $dynamic->getLatitude())) {
                    if (count($points) > 0) {
                        $reversedGeocodeAddress = AddressAdapter::pointToAddress($points[0]);
                    }
                }
                $waypoint->getAddress()->setLongitude($dynamic->getLongitude());
                $waypoint->getAddress()->setLatitude($dynamic->getLatitude());
                if (isset($reversedGeocodeAddress)) {
                    $waypoint->getAddress()->setStreetAddress($reversedGeocodeAddress->getStreetAddress());
                    $waypoint->getAddress()->setPostalCode($reversedGeocodeAddress->getPostalCode());
                    $waypoint->getAddress()->setAddressLocality($reversedGeocodeAddress->getAddressLocality());
                    $waypoint->getAddress()->setAddressCountry($reversedGeocodeAddress->getAddressCountry());
                    $waypoint->getAddress()->setElevation($reversedGeocodeAddress->getElevation());
                    $waypoint->getAddress()->setHouseNumber($reversedGeocodeAddress->getHouseNumber());
                    $waypoint->getAddress()->setStreet($reversedGeocodeAddress->getStreet());
                    $waypoint->getAddress()->setSubLocality($reversedGeocodeAddress->getSubLocality());
                    $waypoint->getAddress()->setLocalAdmin($reversedGeocodeAddress->getLocalAdmin());
                    $waypoint->getAddress()->setCounty($reversedGeocodeAddress->getCounty());
                    $waypoint->getAddress()->setMacroCounty($reversedGeocodeAddress->getMacroCounty());
                    $waypoint->getAddress()->setRegion($reversedGeocodeAddress->getRegion());
                    $waypoint->getAddress()->setMacroRegion($reversedGeocodeAddress->getMacroRegion());
                    $waypoint->getAddress()->setCountryCode($reversedGeocodeAddress->getCountryCode());
                    $waypoint->getAddress()->setVenue($reversedGeocodeAddress->getVenue());
                }

                $this->entityManager->persist($waypoint);
                $this->entityManager->flush();
            }
        }

        // we update the direction of the position

        // first we get all the past points that are stored as a linestring in the geoJsonPoints property
        $points = $dynamic->getProposal()->getPosition()->getGeoJsonPoints()->getPoints();
        // then we add the last point (must be an object that have longitude and latitude properties, like an Address)
        $address = new Address();
        $address->setLatitude($dynamic->getLatitude());
        $address->setLongitude($dynamic->getLongitude());
        $points[] = $address;
        $dynamic->getProposal()->getPosition()->setPoints($points);
        // here we force the update because maybe none of the properties from the entity could be updated, but we need to compute GeoJson
        $dynamic->getProposal()->getPosition()->setAutoUpdatedDate();

        // we create an array of Addresses to compute the real direction using the georouter
        $addresses = [];
        foreach ($points as $point) {
            $waypoint = new Address();
            $waypoint->setLatitude($point->getLatitude());
            $waypoint->setLongitude($point->getLongitude());
            $addresses[] = $waypoint;
        }
        if ($routes = $this->geoRouter->getRoutes($addresses)) {
            // we have a direction
            /**
             * @var Direction $newDirection
             */
            $newDirection = $routes[0];
            $dynamic->getProposal()->getPosition()->getDirection()->setDistance($newDirection->getDistance());
            $dynamic->getProposal()->getPosition()->getDirection()->setDuration($newDirection->getDuration());
            $dynamic->getProposal()->getPosition()->getDirection()->setAscend($newDirection->getAscend());
            $dynamic->getProposal()->getPosition()->getDirection()->setDescend($newDirection->getDescend());
            $dynamic->getProposal()->getPosition()->getDirection()->setBboxMinLon($newDirection->getBboxMinLon());
            $dynamic->getProposal()->getPosition()->getDirection()->setBboxMinLat($newDirection->getBboxMinLat());
            $dynamic->getProposal()->getPosition()->getDirection()->setBboxMaxLon($newDirection->getBboxMaxLon());
            $dynamic->getProposal()->getPosition()->getDirection()->setBboxMaxLat($newDirection->getBboxMaxLat());
            // $dynamic->getProposal()->getPosition()->getDirection()->setDetail($newDirection->getDetail());
            $dynamic->getProposal()->getPosition()->getDirection()->setFormat($newDirection->getFormat());
            $dynamic->getProposal()->getPosition()->getDirection()->setSnapped($newDirection->getSnapped());
            $dynamic->getProposal()->getPosition()->getDirection()->setBearing($newDirection->getBearing());
            // the following is needed to compute the geoJson in the direction automatic update trigger
            $dynamic->getProposal()->getPosition()->getDirection()->setPoints($routes[0]->getPoints());
            $dynamic->getProposal()->getPosition()->getDirection()->setSaveGeoJson(true);
            $dynamic->getProposal()->getPosition()->getDirection()->setDetailUpdatable(true);
            // here we force the update because maybe none of the properties from the entity could be updated, but we need to compute GeoJson
            $dynamic->getProposal()->getPosition()->getDirection()->setAutoUpdatedDate();
        } else {
            // the last point introduced an error as we couldn't compute the direction !
            // we send an exception...
            throw new DynamicException('Bad geographic position... Point ignored !');
        }

        // update the matchings
        // (and update the proposal direction (= direction from the current point to the destination))
        $dynamic->setProposal($this->proposalManager->updateMatchingsForProposal($dynamic->getProposal(), $address));

        // update the proof if there's one pending
        $this->updateProofsDirectionForDynamic($dynamic);

        // persist the updates
        $this->entityManager->persist($dynamic->getProposal());
        $this->entityManager->flush();

        // default order
        $dynamic->setFilters([
            'order' => [
                'criteria' => 'date',
                'value' => 'ASC',
            ],
        ]);

        $dynamic->setResults(
            $this->resultManager->orderResults(
                $this->resultManager->filterResults(
                    $this->resultManager->createAdResults($dynamic->getProposal()),
                    $dynamic->getFilters()
                ),
                $dynamic->getFilters()
            )
        );

        // we get the asks related to the dynamic ad
        // we include the corresponding result
        $asks = [];
        if (Dynamic::ROLE_DRIVER == $dynamic->getRole()) {
            // the user is driver, we search the matching requests
            foreach ($dynamic->getProposal()->getMatchingRequests() as $matching) {
                foreach ($matching->getAsks() as $ask) {
                    /**
                     * @var Ask $ask
                     */
                    // there's an ask, the initiator of the ask is the passenger => the user of the ask
                    // if the pickup hasn't been made yet, we compute the direction between the driver and the passenger
                    $pickUpDuration = null;
                    $pickUpDistance = null;
                    $pickUpUnlock = false;
                    if (0 == count($ask->getCarpoolProofs())) {
                        $addresses = [];
                        $addressDriver = $matching->getProposalOffer()->getPosition()->getWaypoint()->getAddress();
                        $addressPassenger = $matching->getProposalRequest()->getPosition()->getWaypoint()->getAddress();
                        $addresses[] = $addressDriver;
                        $addresses[] = $addressPassenger;
                        $pickUpUnlock = $this->geoTools->haversineGreatCircleDistance(
                            $addressDriver->getLatitude(),
                            $addressDriver->getLongitude(),
                            $addressPassenger->getLatitude(),
                            $addressPassenger->getLongitude()
                        ) <= $this->params['dynamicProofDistance'];
                        if ($routes = $this->geoRouter->getRoutes($addresses)) {
                            $pickUpDuration = $routes[0]->getDuration();
                            $pickUpDistance = $routes[0]->getDistance();
                        }
                    }
                    // check if there's a proof pending
                    $proof = null;
                    if (1 == count($ask->getCarpoolProofs())) {
                        $proof['id'] = $ask->getCarpoolProofs()[0]->getId();
                        if (is_null($ask->getCarpoolProofs()[0]->getPickUpDriverAddress()) && !is_null($ask->getCarpoolProofs()[0]->getPickUpPassengerAddress())) {
                            $proof['needed'] = 'pickUp';
                        } elseif (is_null($ask->getCarpoolProofs()[0]->getDropOffDriverAddress()) && !is_null($ask->getCarpoolProofs()[0]->getDropOffPassengerAddress())) {
                            $proof['needed'] = 'dropOff';
                        }
                    }
                    $status = DynamicAsk::STATUS_PENDING;
                    if (Ask::STATUS_ACCEPTED_AS_DRIVER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_ACCEPTED;
                    } elseif (Ask::STATUS_DECLINED_AS_DRIVER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_DECLINED;
                    } elseif (Ask::STATUS_DECLINED_AS_PASSENGER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_CANCELLED;
                    }
                    $asks[] = [
                        'id' => $ask->getId(),
                        'status' => $status,
                        'user' => [
                            'id' => $ask->getUser()->getId(),
                            'givenName' => $ask->getUser()->getGivenName(),
                            'shortFamilyName' => $ask->getUser()->getShortFamilyName(),
                            'telephone' => DynamicAsk::STATUS_ACCEPTED == $status ? $ask->getUser()->getTelephone() : null,
                            'position' => $matching->getProposalRequest()->getPosition()->getWaypoint()->getAddress(),
                        ],
                        'result' => $this->getResult($matching, $dynamic->getResults()),
                        'messages' => $this->getThread($ask),
                        'priceKm' => $ask->getCriteria()->getPriceKm(),
                        'price' => $ask->getCriteria()->getPassengerComputedRoundedPrice(),
                        'duration' => $matching->getDropOffDuration() - $matching->getPickUpDuration(),
                        'pickUpDuration' => $pickUpDuration,
                        'pickUpDistance' => $pickUpDistance,
                        'pickUpUnlock' => $pickUpUnlock,
                        'detourDistance' => $matching->getDetourDistance(),
                        'detourDuration' => $matching->getDetourDuration(),
                        'proof' => $proof,
                    ];
                }
            }
        } else {
            // the user is passenger, we search the matching offers
            foreach ($dynamic->getProposal()->getMatchingOffers() as $matching) {
                foreach ($matching->getAsks() as $ask) {
                    /**
                     * @var Ask $ask
                     */
                    // there's an ask, the recipient of the ask is the driver => the userRelated of the ask
                    // if the pickup hasn't been made yet, we compute the direction between the driver and the passenger
                    $pickUpDuration = null;
                    $pickUpDistance = null;
                    $pickUpUnlock = false;
                    if (0 == count($ask->getCarpoolProofs())) {
                        $addresses = [];
                        $addressDriver = $matching->getProposalOffer()->getPosition()->getWaypoint()->getAddress();
                        $addressPassenger = $matching->getProposalRequest()->getPosition()->getWaypoint()->getAddress();
                        $addresses[] = $addressDriver;
                        $addresses[] = $addressPassenger;
                        $pickUpUnlock = $this->geoTools->haversineGreatCircleDistance(
                            $addressDriver->getLatitude(),
                            $addressDriver->getLongitude(),
                            $addressPassenger->getLatitude(),
                            $addressPassenger->getLongitude()
                        ) <= $this->params['dynamicProofDistance'];
                        if ($routes = $this->geoRouter->getRoutes($addresses)) {
                            $pickUpDuration = $routes[0]->getDuration();
                            $pickUpDistance = $routes[0]->getDistance();
                        }
                    }
                    // check if there's a proof pending
                    $proof = null;
                    if (1 == count($ask->getCarpoolProofs())) {
                        $proof['id'] = $ask->getCarpoolProofs()[0]->getId();
                        if (!is_null($ask->getCarpoolProofs()[0]->getPickUpDriverAddress()) && is_null($ask->getCarpoolProofs()[0]->getPickUpPassengerAddress())) {
                            $proof['needed'] = 'pickUp';
                        } elseif (!is_null($ask->getCarpoolProofs()[0]->getDropOffDriverAddress()) && is_null($ask->getCarpoolProofs()[0]->getDropOffPassengerAddress())) {
                            $proof['needed'] = 'dropOff';
                        }
                    }
                    $status = DynamicAsk::STATUS_PENDING;
                    if (Ask::STATUS_ACCEPTED_AS_DRIVER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_ACCEPTED;
                    } elseif (Ask::STATUS_DECLINED_AS_DRIVER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_DECLINED;
                    } elseif (Ask::STATUS_DECLINED_AS_PASSENGER == $ask->getStatus()) {
                        $status = DynamicAsk::STATUS_CANCELLED;
                    }
                    $asks[] = [
                        'id' => $ask->getId(),
                        'status' => $status,
                        'user' => [
                            'id' => $ask->getUserRelated()->getId(),
                            'givenName' => $ask->getUserRelated()->getGivenName(),
                            'shortFamilyName' => $ask->getUserRelated()->getShortFamilyName(),
                            'telephone' => DynamicAsk::STATUS_ACCEPTED == $status ? $ask->getUserRelated()->getTelephone() : null,
                            'position' => $matching->getProposalOffer()->getPosition()->getWaypoint()->getAddress(),
                        ],
                        'result' => $this->getResult($matching, $dynamic->getResults()),
                        'messages' => $this->getThread($ask),
                        'priceKm' => $ask->getCriteria()->getPriceKm(),
                        'price' => $ask->getCriteria()->getPassengerComputedRoundedPrice(),
                        'duration' => $matching->getDropOffDuration() - $matching->getPickUpDuration(),
                        'pickUpDuration' => $pickUpDuration,
                        'pickUpDistance' => $pickUpDistance,
                        'pickUpUnlock' => $pickUpUnlock,
                        'detourDistance' => $matching->getDetourDistance(),
                        'detourDuration' => $matching->getDetourDuration(),
                        'proof' => $proof,
                    ];
                }
            }
        }
        $dynamic->setAsks($asks);

        return $dynamic;
    }

    /**
     * Get the last unfinished dynamic ad.
     *
     * @param User $user The user for which we want the ad
     *
     * @return null|Dynamic the dynamic ad found or null if not found
     */
    public function getLastDynamicUnfinished(User $user)
    {
        if ($proposal = $this->proposalManager->getLastDynamicUnfinished($user)) {
            $dynamic = new Dynamic();
            $dynamic->setProposal($proposal);
            $dynamic->setUser($proposal->getUser());
            $dynamic->setRole($proposal->getCriteria()->isDriver() ? Dynamic::ROLE_DRIVER : Dynamic::ROLE_PASSENGER);
            $dynamic->setId($proposal->getId());

            return $dynamic;
        }

        return null;
    }

    // DYNAMIC ASK

    /**
     * Get a dynamic ask.
     *
     * @param int $id the dynamic ask id
     *
     * @return DynamicAsk the dynamic ask
     */
    public function getDynamicAsk(int $id)
    {
        if (!$ask = $this->askRespository->find($id)) {
            throw new DynamicException('Dynamic ask not found');
        }
        $dynamicAsk = new DynamicAsk();
        $dynamicAsk->setId($ask->getId());
        $dynamicAsk->setUser($ask->getUser());
        $dynamicAsk->setCarpooler($ask->getUserRelated());
        $dynamicAsk->setMatchingId($ask->getMatching()->getId());
        $dynamicAsk->setStatus($ask->getStatus());

        return $dynamicAsk;
    }

    /**
     * Create an ask for a dynamic ad.
     *
     * @param DynamicAsk $dynamicAsk The ask to create
     *
     * @return DynamicAsk the created ask
     */
    public function createDynamicAsk(DynamicAsk $dynamicAsk)
    {
        // only the passenger can create an ask
        $matching = $this->matchingRepository->find($dynamicAsk->getMatchingId());
        if ($dynamicAsk->getUser()->getId() != $matching->getProposalRequest()->getUser()->getId()) {
            throw new DynamicException('Only the passenger can create the dynamic ask');
        }

        // check that another ask is not already made
        if ($this->askManager->hasPendingDynamicAsk($dynamicAsk->getUser())) {
            throw new DynamicException('This user has already a pending dynamic ask');
        }

        // check that another ask has not been made on this particular ad
        if ($this->askManager->hasRefusedDynamicAsk($dynamicAsk->getUser(), $matching)) {
            throw new DynamicException('This user has already a refused dynamic ask on this matching');
        }

        $ask = new Ask();
        $ask->setStatus(Ask::STATUS_PENDING_AS_PASSENGER);
        $ask->setType(Proposal::TYPE_ONE_WAY);
        $ask->setUser($dynamicAsk->getUser());
        $ask->setMatching($matching);
        $ask->setUserRelated($matching->getProposalOffer()->getUser());

        // we use the matching criteria
        $criteria = clone $matching->getCriteria();
        $ask->setCriteria($criteria);

        // we use the matching waypoints
        $waypoints = $matching->getWaypoints();
        foreach ($waypoints as $waypoint) {
            $newWaypoint = clone $waypoint;
            $ask->addWaypoint($newWaypoint);
        }

        // Ask History
        $askHistory = new AskHistory();
        $askHistory->setStatus($ask->getStatus());
        $askHistory->setType($ask->getType());
        $ask->addAskHistory($askHistory);

        // message
        if (!is_null($dynamicAsk->getMessage()) && '' != $dynamicAsk->getMessage()) {
            $message = new Message();
            $message->setUser($dynamicAsk->getUser());
            $message->setText($dynamicAsk->getMessage());
            $recipient = new Recipient();
            $recipient->setUser($ask->getUserRelated());
            $recipient->setStatus(Recipient::STATUS_PENDING);
            $message->addRecipient($recipient);
            $this->entityManager->persist($message);
            $askHistory->setMessage($message);
        }

        $this->entityManager->persist($ask);

        // disable the passenger dynamic ad to avoid asks to other drivers
        $matching->getProposalRequest()->setActive(false);
        $this->entityManager->persist($matching->getProposalRequest());

        $this->entityManager->flush();

        // todo : dispatch en event ?

        $dynamicAsk->setId($ask->getId());
        $dynamicAsk->setStatus(DynamicAsk::STATUS_PENDING);

        return $dynamicAsk;
    }

    /**
     * Update an ask for a dynamic ad :
     * - by the driver to accept / refuse an ask
     * - by the passenger to cancel an ask (before the driver has accepted only !).
     *
     * @param int        $id             The id of the ask to update
     * @param DynamicAsk $dynamicAskData The ask data to make the update
     *
     * @return DynamicAsk the updated ask
     */
    public function updateDynamicAsk(int $id, DynamicAsk $dynamicAskData)
    {
        // get the ask
        $ask = $this->askRespository->find($id);

        // the driver should only accept or decline the ask
        if ($ask->getUserRelated()->getId() == $dynamicAskData->getUser()->getId() && DynamicAsk::STATUS_ACCEPTED != $dynamicAskData->getStatus() && DynamicAsk::STATUS_DECLINED != $dynamicAskData->getStatus()) {
            throw new DynamicException('Only accept or decline are permitted.');
        }

        // the driver should only accept or decline a pending ask
        if ($ask->getUserRelated()->getId() == $dynamicAskData->getUser()->getId() && Ask::STATUS_DECLINED_AS_PASSENGER == $ask->getStatus()) {
            throw new DynamicException('The ask has been cancelled.');
        }

        // the passenger can only cancel an ask
        if ($ask->getUser()->getId() == $dynamicAskData->getUser()->getId()) {
            if (Ask::STATUS_ACCEPTED_AS_DRIVER == $ask->getStatus()) {
                throw new DynamicException('Update forbidden : the driver has already accepted the carpooling.');
            }
            if (DynamicAsk::STATUS_CANCELLED != $dynamicAskData->getStatus()) {
                throw new DynamicException('Only cancel is permitted.');
            }
        }

        $ask->setStatus(DynamicAsk::STATUS_ACCEPTED == $dynamicAskData->getStatus() ? Ask::STATUS_ACCEPTED_AS_DRIVER : (DynamicAsk::STATUS_DECLINED == $dynamicAskData->getStatus() ? Ask::STATUS_DECLINED_AS_DRIVER : Ask::STATUS_DECLINED_AS_PASSENGER));
        $dynamicAskData->setId($id);

        // Ask History
        $askHistory = new AskHistory();
        $askHistory->setStatus($ask->getStatus());
        $askHistory->setType($ask->getType());
        $ask->addAskHistory($askHistory);

        // message => the driver is the userRelated, the passenger is the user
        if (!is_null($dynamicAskData->getMessage()) && '' != $dynamicAskData->getMessage()) {
            $message = new Message();
            $message->setText($dynamicAskData->getMessage());
            // we search the previous message if it exists
            if ($lastAskHistoryWithMessage = $this->askHistoryRepository->findLastAskHistoryWithMessage($ask)) {
                if (!is_null($lastAskHistoryWithMessage->getMessage()->getMessage())) {
                    // the linked message has a parent => it is also the parent of our new message
                    $message->setMessage($lastAskHistoryWithMessage->getMessage()->getMessage());
                } else {
                    // no parent => we use the message linked as parent for our new message
                    $message->setMessage($lastAskHistoryWithMessage->getMessage());
                }
            }
            $recipient = new Recipient();
            if ($ask->getUser()->getId() == $dynamicAskData->getUser()->getId()) {
                // the passenger sends a message
                $message->setUser($ask->getUser());
                $recipient->setUser($ask->getUserRelated());
            } else {
                // the driver sends a message
                $message->setUser($ask->getUserRelated());
                $recipient->setUser($ask->getUser());
            }
            $recipient->setStatus(Recipient::STATUS_PENDING);
            $message->addRecipient($recipient);
            $this->entityManager->persist($message);
            $askHistory->setMessage($message);
        }

        $this->entityManager->persist($ask);

        if (Ask::STATUS_ACCEPTED_AS_DRIVER == $ask->getStatus()) {
            // dynamic carpooling accepted : update the ad to include the passenger path

            $proposal = $ask->getMatching()->getProposalOffer();

            // waypoints :
            // - we remove all the previous waypoints
            // - we use the waypoints of the ask
            $newWaypoints = [];
            foreach ($ask->getWaypoints() as $point) {
                $waypoint = clone $point;
                if (0 == $waypoint->getPosition()) {
                    // the first waypoint was the driver floating waypoint when the passenger made the ask, it wasn't reached, but we set it as reached anyway
                    $waypoint->setReached(true);
                }
                // we search in the original waypoints if the current waypoint has been reached by the driver
                foreach ($proposal->getWaypoints() as $curWaypoint) {
                    if (
                        $curWaypoint->getAddress()->getLongitude() == $point->getAddress()->getLongitude()
                        && $curWaypoint->getAddress()->getLatitude() == $point->getAddress()->getLatitude()
                    ) {
                        if ($curWaypoint->isReached()) {
                            $waypoint->setReached(true);
                        }

                        break;
                    }
                }
                $newWaypoints[] = $waypoint;
            }
            foreach ($proposal->getWaypoints() as $waypoint) {
                if (!$waypoint->isFloating()) {
                    $proposal->removeWaypoint($waypoint);
                }
            }
            foreach ($newWaypoints as $waypoint) {
                $proposal->addWaypoint($waypoint);
            }

            // uncomment to cancel the other asks arbitrary as the path has changed
            // foreach ($proposal->getMatchingRequests() as $matching) {
            //     if ($matching->getId() != $ask->getMatching()->getId()) {
            //         foreach ($matching->getAsks() as $ask) {
            //             if ($ask->getStatus() == Ask::STATUS_PENDING_AS_PASSENGER) {
            //                 $ask->setStatus(Ask::STATUS_DECLINED_AS_DRIVER);
            //                 $this->entityManager->persist($ask);
            //             }
            //         }
            //     }
            // }

            // update the matchings
            $this->proposalMatcher->updateMatchingsForProposal($proposal);

            // persist the updates
            $this->entityManager->persist($proposal);
        } else {
            // dynamic carpooling refused or cancelled : update the passenger ad to make it active again
            $ask->getMatching()->getProposalRequest()->setActive(true);
            $this->entityManager->persist($ask->getMatching()->getProposalRequest());
        }

        $this->entityManager->flush();

        return $dynamicAskData;
    }

    // DYNAMIC PROOF

    /**
     * Create a proof for a dynamic ask.
     *
     * @param DynamicProof $dynamicProof The proof to create (or update if it already exists)
     *
     * @return DynamicProof the created or updated proof
     */
    public function createDynamicProof(DynamicProof $dynamicProof)
    {
        // search the ask
        if (!$ask = $this->askRespository->find($dynamicProof->getDynamicAskId())) {
            throw new DynamicException('Dynamic ask not found');
        }

        // check that the ask is accepted
        if (Ask::STATUS_ACCEPTED_AS_DRIVER == !$ask->getStatus()) {
            throw new DynamicException('Dynamic ask not accepted');
        }

        // check if a proof already exists => the array of carpool proofs for the ask has only one item as it's dynamic => punctual
        if (1 == count($ask->getCarpoolProofs())) {
            // the proof already exists, it's an update
            return $this->updateDynamicProof($ask->getCarpoolProofs()[0]->getId(), $dynamicProof);
        }

        $carpoolProof = $this->proofManager->createProof($ask, $dynamicProof->getLongitude(), $dynamicProof->getLatitude(), CarpoolProof::TYPE_UNDETERMINED_DYNAMIC, $dynamicProof->getUser(), $ask->getUserRelated(), $ask->getUser(), $dynamicProof->getDriverPhoneUniqueId(), $dynamicProof->getPassengerPhoneUniqueId());

        $dynamicProof->setId($carpoolProof->getId());
        $dynamicProof->setStatus(
            ($carpoolProof->getPickUpPassengerDate() ? '1' : '0').
            ($carpoolProof->getPickUpDriverDate() ? '1' : '0').
            ($carpoolProof->getDropOffPassengerDate() ? '1' : '0').
            ($carpoolProof->getDropOffDriverDate() ? '1' : '0')
        );

        return $dynamicProof;
    }

    /**
     * Update a dynamic proof.
     *
     * @param int          $id               The id of the dynamic proof to update
     * @param DynamicProof $dynamicProofData The data to update the dynamic proof
     *
     * @return DynamicProof The dynamic proof updated
     */
    public function updateDynamicProof(int $id, DynamicProof $dynamicProofData)
    {
        // search the proof
        if (!$carpoolProof = $this->proofManager->getProof($id)) {
            throw new DynamicException('Dynamic proof not found');
        }

        // Check if the proof has been canceled
        if (CarpoolProof::STATUS_CANCELED === $carpoolProof->getStatus()) {
            throw new DynamicException('Dynamic proof already canceled');
        }

        try {
            $carpoolProof = $this->proofManager->updateProof($id, $dynamicProofData->getLongitude(), $dynamicProofData->getLatitude(), $dynamicProofData->getUser(), $carpoolProof->getAsk()->getMatching()->getProposalRequest()->getUser(), $this->params['dynamicProofDistance'], $dynamicProofData->getDriverPhoneUniqueId(), $dynamicProofData->getPassengerPhoneUniqueId());
            $dynamicProofData->setId($carpoolProof->getId());
            $dynamicProofData->setStatus(
                ($carpoolProof->getPickUpPassengerDate() ? '1' : '0').
                ($carpoolProof->getPickUpDriverDate() ? '1' : '0').
                ($carpoolProof->getDropOffPassengerDate() ? '1' : '0').
                ($carpoolProof->getDropOffDriverDate() ? '1' : '0')
            );
        } catch (ProofException $proofException) {
            throw new DynamicException($proofException->getMessage());
        }

        return $dynamicProofData;
    }

    /**
     * Get a result from a Matching.
     *
     * @param Matching $matching The matching
     * @param array    $results  The array of results
     *
     * @return null|Result The result found
     */
    private function getResult(Matching $matching, array $results)
    {
        foreach ($results as $result) {
            /**
             * @var Result $result
             */
            if (!is_null($result->getResultDriver()) && !is_null($result->getResultDriver()->getOutward())) {
                if ($result->getResultDriver()->getOutward()->getMatchingId() == $matching->getId()) {
                    return $result;
                }
            }
            if (!is_null($result->getResultPassenger()) && !is_null($result->getResultPassenger()->getOutward())) {
                if ($result->getResultPassenger()->getOutward()->getMatchingId() == $matching->getId()) {
                    return $result;
                }
            }
        }

        return null;
    }

    /**
     * Get all the messages related to an ask.
     *
     * @param Ask $ask The ask
     *
     * @return array The messages
     */
    private function getThread(Ask $ask)
    {
        $thread = [];
        if (!is_null($ask->getAskHistories()[0]->getMessage())) {
            $messages = $this->internalMessageManager->getCompleteThread($ask->getAskHistories()[0]->getMessage()->getId());
            foreach ($messages as $message) {
                // @var Message $message
                $thread[] = [
                    'text' => $message->getText(),
                    'user' => [
                        'id' => $message->getUser()->getId(),
                        'givenName' => $message->getUser()->getGivenName(),
                        'shortFamilyName' => $message->getUser()->getShortFamilyName(),
                    ],
                ];
            }
        }

        return $thread;
    }

    /**
     * Update the direction of the related proofs of a dynamic ad (if it exists).
     * For now we only update the direction using the driver position updates to avoid mismatches.
     *
     * @param Dynamic $dynamic The dynamic ad
     */
    private function updateProofsDirectionForDynamic(Dynamic $dynamic)
    {
        // first we search if there are asks related to the dynamic ad
        if (Dynamic::ROLE_DRIVER == $dynamic->getRole()) {
            // the user is driver
            foreach ($dynamic->getProposal()->getMatchingRequests() as $matching) {
                /**
                 * @var Matching $matching
                 */
                foreach ($matching->getAsks() as $ask) {
                    /**
                     * @var Ask $ask
                     */
                    if (Ask::STATUS_ACCEPTED_AS_DRIVER == $ask->getStatus() && 1 == count($ask->getCarpoolProofs()) && !is_null($ask->getCarpoolProofs()[0]->getPickUpDriverAddress())) {
                        // we update the direction if the driver has made its pickup certification
                        $this->updateProofDirection($ask->getCarpoolProofs()[0], $dynamic->getLongitude(), $dynamic->getLatitude());
                    }
                }
            }
        }
        // uncomment the following to use also the passenger position
        // } else {
        //     // the user is passenger
        //     foreach ($dynamic->getProposal()->getMatchingOffers() as $matching) {
        //         /**
        //          * @var Matching $matching
        //          */
        //         foreach ($matching->getAsks() as $ask) {
        //             /**
        //              * @var Ask $ask
        //              */
        //             if ($ask->getStatus() == Ask::STATUS_ACCEPTED_AS_DRIVER && !is_null($ask->getCarpoolProof()) && !is_null($ask->getCarpoolProof()->getPickUpPassengerAddress())) {
        //                 $this->updateProofDirection($ask->getCarpoolProof(),$dynamic->getLongitude(), $dynamic->getLatitude());
        //             }
        //         }
        //     }
        // }
    }

    /**
     * Update a carpool proof direction.
     *
     * @param CarpoolProof $carpoolProof The carpool proof
     * @param float        $longitude    The longitude of the new point
     * @param float        $latitude     The latitude of the new point
     */
    private function updateProofDirection(CarpoolProof $carpoolProof, float $longitude, float $latitude)
    {
        // first we get all the past points that are stored as a linestring in the geoJsonPoints property
        $points = $carpoolProof->getGeoJsonPoints()->getPoints();
        // then we add the last point (must be an object that have longitude and latitude properties, like an Address)
        $address = new Address();
        $address->setLatitude($latitude);
        $address->setLongitude($longitude);
        $points[] = $address;
        $carpoolProof->setPoints($points);
        // here we force the update because maybe none of the properties from the entity could be updated, but we need to compute GeoJson
        $carpoolProof->setAutoUpdatedDate();

        // we create an array of Addresses to compute the real direction using the georouter
        $addresses = [];
        foreach ($points as $point) {
            $waypoint = new Address();
            $waypoint->setLatitude($point->getLatitude());
            $waypoint->setLongitude($point->getLongitude());
            $addresses[] = $waypoint;
        }
        if ($routes = $this->geoRouter->getRoutes($addresses)) {
            // we have a direction
            /**
             * @var Direction $newDirection
             */
            $newDirection = $routes[0];
            $carpoolProof->getDirection()->setDistance($newDirection->getDistance());
            $carpoolProof->getDirection()->setDuration($newDirection->getDuration());
            $carpoolProof->getDirection()->setAscend($newDirection->getAscend());
            $carpoolProof->getDirection()->setDescend($newDirection->getDescend());
            $carpoolProof->getDirection()->setBboxMinLon($newDirection->getBboxMinLon());
            $carpoolProof->getDirection()->setBboxMinLat($newDirection->getBboxMinLat());
            $carpoolProof->getDirection()->setBboxMaxLon($newDirection->getBboxMaxLon());
            $carpoolProof->getDirection()->setBboxMaxLat($newDirection->getBboxMaxLat());
            // $carpoolProof->getDirection()->setDetail($newDirection->getDetail());
            $carpoolProof->getDirection()->setFormat($newDirection->getFormat());
            $carpoolProof->getDirection()->setSnapped($newDirection->getSnapped());
            $carpoolProof->getDirection()->setBearing($newDirection->getBearing());
            // the following is needed to compute the geoJson in the direction automatic update trigger
            $carpoolProof->getDirection()->setPoints($routes[0]->getPoints());
            $carpoolProof->getDirection()->setSaveGeoJson(true);
            $carpoolProof->getDirection()->setDetailUpdatable(true);
            // here we force the update because maybe none of the properties from the entity could be updated, but we need to compute GeoJson
            $carpoolProof->getDirection()->setAutoUpdatedDate();
        } else {
            // the last point introduced an error as we couldn't compute the direction !
            // we send an exeption...
            throw new DynamicException('Bad geographic position... Point ignored !');
        }

        $this->entityManager->persist($carpoolProof);
        $this->entityManager->flush();
    }
}