<?php
namespace AdminBundle\Services;
use AdminBundle\Entity\Payment;
use AdminBundle\Entity\Transaction;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use AdminBundle\Entity\Booking;
use AdminBundle\Entity\BookingHistory;
use AdminBundle\Entity\CarType;
use AdminBundle\Entity\CarTypePrice;
use AdminBundle\Entity\DriverHistoryLocations;
use AdminBundle\Entity\Settings;
use AdminBundle\Entity\User;
use AdminBundle\Repository\DriverHistoryLocationsRepository;
use AdminBundle\Repository\SettingsRepository;
use AdminBundle\Utils\DateTimeUtils;
use AdminBundle\Utils\DistanceUtil;
class BookingService
{
private const DATE_DIFF_SECONDS = 10;
use LoggerAwareTrait;
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var GoogleApiService
*/
private $googleApiService;
/**
* @param EntityManagerInterface $entityManager
* @param GoogleApiService $googleApiService
* @param LoggerInterface|null $logger
*/
public function __construct(
EntityManagerInterface $entityManager,
GoogleApiService $googleApiService,
LoggerInterface $logger = null
) {
$this->entityManager = $entityManager;
$this->googleApiService = $googleApiService;
$this->logger = $logger ?: new NullLogger();
}
/**
* @param Booking $booking
* @param float $distance
* @param int|null $duration
*
* @return float
*/
public function calculateDiffByBookingForDistanceAndDuration(
Booking $booking,
float $distance,
?int $duration = null
): float {
if ($booking->isFixedPrice()) {
return 0;
}
$carType = $booking->getCarType();
if (!$carType instanceof CarType) {
return 0;
}
$price = 0;
/** @var SettingsRepository $settingsRepository */
$settingsRepository = $this
->entityManager
->getRepository(Settings::class)
;
/** @var Settings|null $setting */
$setting = $settingsRepository->findOneByKey('calculate_quote_type');
if ($setting instanceof Settings) {
switch ($setting->getValue()) {
case Settings::VALUE_CALCULATE_QUOTE_TYPE_OVER_ALL:
$price = $this->calculatePriceForOverAllQuoteType($carType, $distance);
break;
case Settings::VALUE_CALCULATE_QUOTE_TYPE_ADDED:
$price = $this->calculatePriceForAddedQuoteType($carType, $distance);
break;
default:
break;
}
}
// @TODO: Check if this is needed.
$price += $carType->getDynamicPricingCharge();
if (null === $duration || 0 === $carType->getDurationExtraChargeAfter()) {
return $price;
}
$minutes = DateTimeUtils::convertSecondsToMinutes($duration);
if ($carType->getDurationExtraChargeAfter() < $minutes) {
$price += $minutes * $carType->getDurationExtraCharge();
}
return $price;
}
/**
* @param Booking $booking
*
* @return int|null
*/
public function calculateDurationByBooking(Booking $booking): ?int
{
$arrivedAndWaitingHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
$booking,
Booking::STATUS_ARRIVED_AND_WAITING
);
$completedHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
$booking,
Booking::STATUS_COMPLETED
);
if (null === $arrivedAndWaitingHistoryLogDate || null === $completedHistoryLogDate) {
return null;
}
return $completedHistoryLogDate->getTimestamp() - $arrivedAndWaitingHistoryLogDate->getTimestamp();
}
/**
* @param Booking $booking
*
* @return float|null
*/
public function calculateDistanceByBooking(Booking $booking)
{
// if (Booking::STATUS_COMPLETED !== $booking->getStatus()) {
// $this->logger->warning('Booking ID#{id}, KEY#{key}: Booking is not completed.', [
// 'id' => $booking->getId(),
// 'key' => $booking->getKey(),
// 'status' => $booking->getStatusAsText(),
// ]);
//
// return null;
// }
$coords = $this->getCoordsByBooking($booking);
if (count($coords) < 2) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: There are less than 2 coordinates.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'coords' => $coords,
]);
return null;
}
$start = array_shift($coords);
$end = array_pop($coords);
$waypoints = $this->prepareWaypointsByCoords($booking, $coords, GoogleApiService::MAX_WAYPOINTS_NUMBER);
$directionsResponse = $this
->googleApiService
->directionsRequest($start, $end, $waypoints)
;
if (
!array_key_exists('status', $directionsResponse) ||
!array_key_exists('routes', $directionsResponse) ||
GoogleApiService::DIRECTIONS_STATUS_OK !== $directionsResponse['status'] ||
empty($directionsResponse['routes'])
) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: Unsuccessful response from \'directions\'.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'response' => $directionsResponse,
]);
return null;
}
$route = array_shift($directionsResponse['routes']);
if (!array_key_exists('legs', $route)) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: There are no legs for the route.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'route' => $route,
]);
return null;
}
$distance = 0;
foreach ($route['legs'] as $leg) {
$distance += round($leg['distance']['value'] / DistanceUtil::M_IN_KM, 1);
}
return $distance;
}
/**
* @param Booking $booking
*
* @return array
*/
private function getCoordsByBooking(Booking $booking)
{
$arrivedAndWaitingHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
$booking,
Booking::STATUS_ARRIVED_AND_WAITING
);
$completedHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
$booking,
Booking::STATUS_COMPLETED
);
if (null === $arrivedAndWaitingHistoryLogDate || null === $completedHistoryLogDate) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: \'ArrivedAndWaiting\' or \'Completed\' log dates could not be found.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'arrivedAndWaitingLogDate' => $arrivedAndWaitingHistoryLogDate,
'completedLogDate' => $completedHistoryLogDate,
]);
return [];
}
/** @var DriverHistoryLocationsRepository $driverHistoryLocationsRepository */
$driverHistoryLocationsRepository = $this
->entityManager
->getRepository(DriverHistoryLocations::class)
;
$arrivedAndWaitingHistoryLocation = $driverHistoryLocationsRepository->findOneByBookingBetweenDates(
$booking,
$arrivedAndWaitingHistoryLogDate,
(clone $arrivedAndWaitingHistoryLogDate)
->modify(sprintf('+%d seconds', self::DATE_DIFF_SECONDS))
);
$completedHistoryLocation = $driverHistoryLocationsRepository->findOneByBookingBetweenDates(
$booking,
(clone $completedHistoryLogDate)
->modify(sprintf('-%d seconds', self::DATE_DIFF_SECONDS)),
$completedHistoryLogDate
);
if (null === $arrivedAndWaitingHistoryLocation || null === $completedHistoryLocation) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: \'ArrivedAndWaiting\' or \'Completed\' history locations could not be found.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'arrivedAndWaitingHistoryLocation' => $arrivedAndWaitingHistoryLocation
? $arrivedAndWaitingHistoryLocation->getId()
: 'null',
'completedHistoryLocation' => $completedHistoryLocation
? $completedHistoryLocation->getId()
: 'null',
]);
return [];
}
$coords = $driverHistoryLocationsRepository->findDistinctCoordsByBookingBetweenDates(
$booking,
$arrivedAndWaitingHistoryLocation->getDate(),
$completedHistoryLocation->getDate()
);
$result = [];
foreach ($coords as $item) {
$result[] = [
'lat' => $item['lng'],
'lng' => $item['lat'],
];
}
return $result;
}
/**
* @param Booking $booking
* @param array $coords
* @param int $limit
*
* @return array
*/
private function prepareWaypointsByCoords(Booking $booking, array $coords, $limit)
{
if (empty($coords)) {
return [];
}
$waypoints = [];
$chunks = array_chunk($coords, ceil(count($coords) / $limit));
foreach ($chunks as $chunk) {
$middle = $chunk[round((count($chunk) - 1) / 2, 0)];
$waypoints[] = [
'lat' => $middle['lat'],
'lng' => $middle['lng'],
];
}
$snappedPointsResponse = $this
->googleApiService
->snapToRoadsRequest($waypoints)
;
if (!array_key_exists('snappedPoints', $snappedPointsResponse)) {
$this->logger->warning('Booking ID#{id}, KEY#{key}: Unsuccessful response from \'snap-to-roads\'.', [
'id' => $booking->getId(),
'key' => $booking->getKey(),
'response' => $snappedPointsResponse,
]);
return [];
}
$result = [];
foreach ($snappedPointsResponse['snappedPoints'] as $snapedPoint) {
if (!array_key_exists('location', $snapedPoint)) {
continue;
}
$result[] = [
'lat' => $snapedPoint['location']['latitude'],
'lng' => $snapedPoint['location']['longitude'],
];
}
return $result;
}
/**
* @param Booking $booking
* @param int $status
*
* @return \DateTime|null
*/
private function getChangedStatusHistoryLogDateByBookingAndStatus(Booking $booking, int $status): ?\DateTime
{
/** @var BookingHistory[] $changedStatusHistoryLogs */
$changedStatusHistoryLogs = $this
->entityManager
->getRepository(BookingHistory::class)
->findBy([
'booking' => $booking,
'user' => $booking->getDriver()->getUser(),
'actionType' => BookingHistory::ACTION_TYPE_CHANGED_STATUS,
])
;
foreach ($changedStatusHistoryLogs as $changedStatusHistoryLog) {
$data = $changedStatusHistoryLog->getPayload();
if (!array_key_exists('current_status', $data)) {
continue;
}
if ($status === intval($data['current_status'])) {
return $changedStatusHistoryLog->getDate();
}
}
return null;
}
/**
* @param CarType $carType
* @param float $distance
*
* @return float
*/
private function calculatePriceForOverAllQuoteType(CarType $carType, float $distance): float
{
try {
$pricePerUnit = $this
->entityManager
->getRepository(CarTypePrice::class)
->getOverallThresholdPriceOfCarType($carType->getId(), $distance)
;
} catch (\Exception $exception) {
$pricePerUnit = $this
->entityManager
->getRepository(CarType::class)
->getPricePerUnity($carType->getId())
;
}
return $pricePerUnit * $distance;
}
/**
* @param CarType $carType
* @param float $distance
*
* @return float
*/
private function calculatePriceForAddedQuoteType(CarType $carType, float $distance): float
{
$pricesThresholds = $this
->entityManager
->getRepository(CarTypePrice::class)
->getThresholdPricesOfCarType($carType->getId())
;
$price = 0;
$distanceThresholdTotal = 0;
$startThreshold = 0;
foreach ($pricesThresholds as $priceThreshold) {
$endThreshold = $priceThreshold['end'];
$distanceThreshold = $endThreshold - $startThreshold;
if ($distance > $distanceThresholdTotal + $distanceThreshold) {
$distanceThresholdTotal += $distanceThreshold;
$price += $distanceThreshold * $priceThreshold['value'];
$startThreshold = $endThreshold;
continue;
}
$price += $priceThreshold['value'] * ($distance - $distanceThresholdTotal);
break;
}
return $price;
}
/**
* Get Last Driver Price from history Query Builder
* @param $bookingId
*/
private function lastDriverPriceQB($bookingId) {
$em = $this->entityManager;
$qb = $em->getRepository(BookingHistory::class)
->createQueryBuilder('h')
->select('h.payload')
->where('h.booking = :booking')
->andWhere("h.payload like '%driverPrice%'")
->setParameter('booking', $bookingId);
return $qb;
}
/**
* Get Last Driver Price from history
* @param $bookingId
*/
public function lastDriverPrice($bookingId) {
$driverPrice = null;
$em = $this->entityManager;
$qb = $this->lastDriverPriceQB($bookingId)
->orderBy('h.date', 'DESC')
->setMaxResults(1);
// Last Driver Price change from history.
$payload = $qb->getQuery()->getOneOrNullResult();
if (!$payload) {
$expr = $em->getExpressionBuilder();
$sub = $em->getRepository(User::class)
->createQueryBuilder('u')
->select('u.id')
->where("u.roles like '%OPERATOR%'")
->orWhere("u.roles like '%ADMIN%'");
$qb = $this->lastDriverPriceQB($bookingId)
->andWhere(
$expr->in('h.user', $sub->getDQL())
)
->orderBy('h.date', 'DESC')
->setMaxResults(1);
// Last OP/Admin Driver Price change from history.
$payload = $qb->getQuery()->getOneOrNullResult();
if (!$payload) {
$qb = $this->lastDriverPriceQB($bookingId)
->orderBy('h.date', 'ASC')
->setMaxResults(1);
// First Driver Price from history.
$payload = $qb->getQuery()->getOneOrNullResult();
}
}
if ($payload) {
$payload = current($payload);
if (!is_array($payload)) {
$payload = unserialize($payload);
}
if (isset($payload['priceDetails'])) {
$payload = $payload['priceDetails'];
}
$driverPrice = $payload['driverPrice'];
if (is_array($driverPrice)) {
$driverPrice = $driverPrice[0];
}
}
if (!$driverPrice) {
// Current Driver Price.
$booking = $em->getRepository(Booking::class)->find($bookingId);
if ($booking) {
$driverPrice = $booking->getDriverPrice();
}
}
return $driverPrice;
}
/**
* ADD BOOKinG HISTORY
* @param $booking
* @param $actionType
* @param $user
* @param $payload
*/
public function addBookingHistory($booking, $actionType, $user, $payload)
{
$bookingHistory = new BookingHistory();
$bookingHistory->setBooking($booking);
$bookingHistory->setActionType($actionType);
$bookingHistory->setUser($user);
$bookingHistory->setPayload($payload);
$this->entityManager->persist($bookingHistory);
$this->entityManager->flush();
}
/**
* ADD BOOKinG HISTORY
* @param Payment $payment
* @param $amount
* @param $type
* @param $transactionId
*/
public function addPaymentTransaction($payment, $amount, $type, $transactionId)
{
$settingRepository = $this->entityManager->getRepository(Settings::class);
$this->entityManager->refresh($payment);
$paymentTransaction = new Transaction();
$paymentTransaction->setPayment($payment)
->setAmount($settingRepository->applyRoundTwoDecimal($amount))
->setType($type)
->setCreatedByUser($payment->getBooking()->getClientUser());
switch ($type) {
case Transaction::TYPE_BRAINTREE:
$paymentTransaction->setBraintreeTransactionId($transactionId);
break;
case Transaction::TYPE_STRIPE:
$paymentTransaction->setStripeTransactionId($transactionId);
break;
}
$this->entityManager->persist($paymentTransaction);
$payment->addTransaction($paymentTransaction);
$payment->updateAmountLeft();
$this->entityManager->persist($payment);
$this->entityManager->flush();
}
}