src/AdminBundle/Services/BookingService.php line 44

Open in your IDE?
  1. <?php
  2. namespace AdminBundle\Services;
  3. use AdminBundle\Entity\Payment;
  4. use AdminBundle\Entity\Transaction;
  5. use Doctrine\ORM\EntityManagerInterface;
  6. use Psr\Log\LoggerAwareTrait;
  7. use Psr\Log\LoggerInterface;
  8. use Psr\Log\NullLogger;
  9. use AdminBundle\Entity\Booking;
  10. use AdminBundle\Entity\BookingHistory;
  11. use AdminBundle\Entity\CarType;
  12. use AdminBundle\Entity\CarTypePrice;
  13. use AdminBundle\Entity\DriverHistoryLocations;
  14. use AdminBundle\Entity\Settings;
  15. use AdminBundle\Entity\User;
  16. use AdminBundle\Repository\DriverHistoryLocationsRepository;
  17. use AdminBundle\Repository\SettingsRepository;
  18. use AdminBundle\Utils\DateTimeUtils;
  19. use AdminBundle\Utils\DistanceUtil;
  20. class BookingService
  21. {
  22. private const DATE_DIFF_SECONDS = 10;
  23. use LoggerAwareTrait;
  24. /**
  25. * @var EntityManagerInterface
  26. */
  27. private $entityManager;
  28. /**
  29. * @var GoogleApiService
  30. */
  31. private $googleApiService;
  32. /**
  33. * @param EntityManagerInterface $entityManager
  34. * @param GoogleApiService $googleApiService
  35. * @param LoggerInterface|null $logger
  36. */
  37. public function __construct(
  38. EntityManagerInterface $entityManager,
  39. GoogleApiService $googleApiService,
  40. LoggerInterface $logger = null
  41. ) {
  42. $this->entityManager = $entityManager;
  43. $this->googleApiService = $googleApiService;
  44. $this->logger = $logger ?: new NullLogger();
  45. }
  46. /**
  47. * @param Booking $booking
  48. * @param float $distance
  49. * @param int|null $duration
  50. *
  51. * @return float
  52. */
  53. public function calculateDiffByBookingForDistanceAndDuration(
  54. Booking $booking,
  55. float $distance,
  56. ?int $duration = null
  57. ): float {
  58. if ($booking->isFixedPrice()) {
  59. return 0;
  60. }
  61. $carType = $booking->getCarType();
  62. if (!$carType instanceof CarType) {
  63. return 0;
  64. }
  65. $price = 0;
  66. /** @var SettingsRepository $settingsRepository */
  67. $settingsRepository = $this
  68. ->entityManager
  69. ->getRepository(Settings::class)
  70. ;
  71. /** @var Settings|null $setting */
  72. $setting = $settingsRepository->findOneByKey('calculate_quote_type');
  73. if ($setting instanceof Settings) {
  74. switch ($setting->getValue()) {
  75. case Settings::VALUE_CALCULATE_QUOTE_TYPE_OVER_ALL:
  76. $price = $this->calculatePriceForOverAllQuoteType($carType, $distance);
  77. break;
  78. case Settings::VALUE_CALCULATE_QUOTE_TYPE_ADDED:
  79. $price = $this->calculatePriceForAddedQuoteType($carType, $distance);
  80. break;
  81. default:
  82. break;
  83. }
  84. }
  85. // @TODO: Check if this is needed.
  86. $price += $carType->getDynamicPricingCharge();
  87. if (null === $duration || 0 === $carType->getDurationExtraChargeAfter()) {
  88. return $price;
  89. }
  90. $minutes = DateTimeUtils::convertSecondsToMinutes($duration);
  91. if ($carType->getDurationExtraChargeAfter() < $minutes) {
  92. $price += $minutes * $carType->getDurationExtraCharge();
  93. }
  94. return $price;
  95. }
  96. /**
  97. * @param Booking $booking
  98. *
  99. * @return int|null
  100. */
  101. public function calculateDurationByBooking(Booking $booking): ?int
  102. {
  103. $arrivedAndWaitingHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
  104. $booking,
  105. Booking::STATUS_ARRIVED_AND_WAITING
  106. );
  107. $completedHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
  108. $booking,
  109. Booking::STATUS_COMPLETED
  110. );
  111. if (null === $arrivedAndWaitingHistoryLogDate || null === $completedHistoryLogDate) {
  112. return null;
  113. }
  114. return $completedHistoryLogDate->getTimestamp() - $arrivedAndWaitingHistoryLogDate->getTimestamp();
  115. }
  116. /**
  117. * @param Booking $booking
  118. *
  119. * @return float|null
  120. */
  121. public function calculateDistanceByBooking(Booking $booking)
  122. {
  123. // if (Booking::STATUS_COMPLETED !== $booking->getStatus()) {
  124. // $this->logger->warning('Booking ID#{id}, KEY#{key}: Booking is not completed.', [
  125. // 'id' => $booking->getId(),
  126. // 'key' => $booking->getKey(),
  127. // 'status' => $booking->getStatusAsText(),
  128. // ]);
  129. //
  130. // return null;
  131. // }
  132. $coords = $this->getCoordsByBooking($booking);
  133. if (count($coords) < 2) {
  134. $this->logger->warning('Booking ID#{id}, KEY#{key}: There are less than 2 coordinates.', [
  135. 'id' => $booking->getId(),
  136. 'key' => $booking->getKey(),
  137. 'coords' => $coords,
  138. ]);
  139. return null;
  140. }
  141. $start = array_shift($coords);
  142. $end = array_pop($coords);
  143. $waypoints = $this->prepareWaypointsByCoords($booking, $coords, GoogleApiService::MAX_WAYPOINTS_NUMBER);
  144. $directionsResponse = $this
  145. ->googleApiService
  146. ->directionsRequest($start, $end, $waypoints)
  147. ;
  148. if (
  149. !array_key_exists('status', $directionsResponse) ||
  150. !array_key_exists('routes', $directionsResponse) ||
  151. GoogleApiService::DIRECTIONS_STATUS_OK !== $directionsResponse['status'] ||
  152. empty($directionsResponse['routes'])
  153. ) {
  154. $this->logger->warning('Booking ID#{id}, KEY#{key}: Unsuccessful response from \'directions\'.', [
  155. 'id' => $booking->getId(),
  156. 'key' => $booking->getKey(),
  157. 'response' => $directionsResponse,
  158. ]);
  159. return null;
  160. }
  161. $route = array_shift($directionsResponse['routes']);
  162. if (!array_key_exists('legs', $route)) {
  163. $this->logger->warning('Booking ID#{id}, KEY#{key}: There are no legs for the route.', [
  164. 'id' => $booking->getId(),
  165. 'key' => $booking->getKey(),
  166. 'route' => $route,
  167. ]);
  168. return null;
  169. }
  170. $distance = 0;
  171. foreach ($route['legs'] as $leg) {
  172. $distance += round($leg['distance']['value'] / DistanceUtil::M_IN_KM, 1);
  173. }
  174. return $distance;
  175. }
  176. /**
  177. * @param Booking $booking
  178. *
  179. * @return array
  180. */
  181. private function getCoordsByBooking(Booking $booking)
  182. {
  183. $arrivedAndWaitingHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
  184. $booking,
  185. Booking::STATUS_ARRIVED_AND_WAITING
  186. );
  187. $completedHistoryLogDate = $this->getChangedStatusHistoryLogDateByBookingAndStatus(
  188. $booking,
  189. Booking::STATUS_COMPLETED
  190. );
  191. if (null === $arrivedAndWaitingHistoryLogDate || null === $completedHistoryLogDate) {
  192. $this->logger->warning('Booking ID#{id}, KEY#{key}: \'ArrivedAndWaiting\' or \'Completed\' log dates could not be found.', [
  193. 'id' => $booking->getId(),
  194. 'key' => $booking->getKey(),
  195. 'arrivedAndWaitingLogDate' => $arrivedAndWaitingHistoryLogDate,
  196. 'completedLogDate' => $completedHistoryLogDate,
  197. ]);
  198. return [];
  199. }
  200. /** @var DriverHistoryLocationsRepository $driverHistoryLocationsRepository */
  201. $driverHistoryLocationsRepository = $this
  202. ->entityManager
  203. ->getRepository(DriverHistoryLocations::class)
  204. ;
  205. $arrivedAndWaitingHistoryLocation = $driverHistoryLocationsRepository->findOneByBookingBetweenDates(
  206. $booking,
  207. $arrivedAndWaitingHistoryLogDate,
  208. (clone $arrivedAndWaitingHistoryLogDate)
  209. ->modify(sprintf('+%d seconds', self::DATE_DIFF_SECONDS))
  210. );
  211. $completedHistoryLocation = $driverHistoryLocationsRepository->findOneByBookingBetweenDates(
  212. $booking,
  213. (clone $completedHistoryLogDate)
  214. ->modify(sprintf('-%d seconds', self::DATE_DIFF_SECONDS)),
  215. $completedHistoryLogDate
  216. );
  217. if (null === $arrivedAndWaitingHistoryLocation || null === $completedHistoryLocation) {
  218. $this->logger->warning('Booking ID#{id}, KEY#{key}: \'ArrivedAndWaiting\' or \'Completed\' history locations could not be found.', [
  219. 'id' => $booking->getId(),
  220. 'key' => $booking->getKey(),
  221. 'arrivedAndWaitingHistoryLocation' => $arrivedAndWaitingHistoryLocation
  222. ? $arrivedAndWaitingHistoryLocation->getId()
  223. : 'null',
  224. 'completedHistoryLocation' => $completedHistoryLocation
  225. ? $completedHistoryLocation->getId()
  226. : 'null',
  227. ]);
  228. return [];
  229. }
  230. $coords = $driverHistoryLocationsRepository->findDistinctCoordsByBookingBetweenDates(
  231. $booking,
  232. $arrivedAndWaitingHistoryLocation->getDate(),
  233. $completedHistoryLocation->getDate()
  234. );
  235. $result = [];
  236. foreach ($coords as $item) {
  237. $result[] = [
  238. 'lat' => $item['lng'],
  239. 'lng' => $item['lat'],
  240. ];
  241. }
  242. return $result;
  243. }
  244. /**
  245. * @param Booking $booking
  246. * @param array $coords
  247. * @param int $limit
  248. *
  249. * @return array
  250. */
  251. private function prepareWaypointsByCoords(Booking $booking, array $coords, $limit)
  252. {
  253. if (empty($coords)) {
  254. return [];
  255. }
  256. $waypoints = [];
  257. $chunks = array_chunk($coords, ceil(count($coords) / $limit));
  258. foreach ($chunks as $chunk) {
  259. $middle = $chunk[round((count($chunk) - 1) / 2, 0)];
  260. $waypoints[] = [
  261. 'lat' => $middle['lat'],
  262. 'lng' => $middle['lng'],
  263. ];
  264. }
  265. $snappedPointsResponse = $this
  266. ->googleApiService
  267. ->snapToRoadsRequest($waypoints)
  268. ;
  269. if (!array_key_exists('snappedPoints', $snappedPointsResponse)) {
  270. $this->logger->warning('Booking ID#{id}, KEY#{key}: Unsuccessful response from \'snap-to-roads\'.', [
  271. 'id' => $booking->getId(),
  272. 'key' => $booking->getKey(),
  273. 'response' => $snappedPointsResponse,
  274. ]);
  275. return [];
  276. }
  277. $result = [];
  278. foreach ($snappedPointsResponse['snappedPoints'] as $snapedPoint) {
  279. if (!array_key_exists('location', $snapedPoint)) {
  280. continue;
  281. }
  282. $result[] = [
  283. 'lat' => $snapedPoint['location']['latitude'],
  284. 'lng' => $snapedPoint['location']['longitude'],
  285. ];
  286. }
  287. return $result;
  288. }
  289. /**
  290. * @param Booking $booking
  291. * @param int $status
  292. *
  293. * @return \DateTime|null
  294. */
  295. private function getChangedStatusHistoryLogDateByBookingAndStatus(Booking $booking, int $status): ?\DateTime
  296. {
  297. /** @var BookingHistory[] $changedStatusHistoryLogs */
  298. $changedStatusHistoryLogs = $this
  299. ->entityManager
  300. ->getRepository(BookingHistory::class)
  301. ->findBy([
  302. 'booking' => $booking,
  303. 'user' => $booking->getDriver()->getUser(),
  304. 'actionType' => BookingHistory::ACTION_TYPE_CHANGED_STATUS,
  305. ])
  306. ;
  307. foreach ($changedStatusHistoryLogs as $changedStatusHistoryLog) {
  308. $data = $changedStatusHistoryLog->getPayload();
  309. if (!array_key_exists('current_status', $data)) {
  310. continue;
  311. }
  312. if ($status === intval($data['current_status'])) {
  313. return $changedStatusHistoryLog->getDate();
  314. }
  315. }
  316. return null;
  317. }
  318. /**
  319. * @param CarType $carType
  320. * @param float $distance
  321. *
  322. * @return float
  323. */
  324. private function calculatePriceForOverAllQuoteType(CarType $carType, float $distance): float
  325. {
  326. try {
  327. $pricePerUnit = $this
  328. ->entityManager
  329. ->getRepository(CarTypePrice::class)
  330. ->getOverallThresholdPriceOfCarType($carType->getId(), $distance)
  331. ;
  332. } catch (\Exception $exception) {
  333. $pricePerUnit = $this
  334. ->entityManager
  335. ->getRepository(CarType::class)
  336. ->getPricePerUnity($carType->getId())
  337. ;
  338. }
  339. return $pricePerUnit * $distance;
  340. }
  341. /**
  342. * @param CarType $carType
  343. * @param float $distance
  344. *
  345. * @return float
  346. */
  347. private function calculatePriceForAddedQuoteType(CarType $carType, float $distance): float
  348. {
  349. $pricesThresholds = $this
  350. ->entityManager
  351. ->getRepository(CarTypePrice::class)
  352. ->getThresholdPricesOfCarType($carType->getId())
  353. ;
  354. $price = 0;
  355. $distanceThresholdTotal = 0;
  356. $startThreshold = 0;
  357. foreach ($pricesThresholds as $priceThreshold) {
  358. $endThreshold = $priceThreshold['end'];
  359. $distanceThreshold = $endThreshold - $startThreshold;
  360. if ($distance > $distanceThresholdTotal + $distanceThreshold) {
  361. $distanceThresholdTotal += $distanceThreshold;
  362. $price += $distanceThreshold * $priceThreshold['value'];
  363. $startThreshold = $endThreshold;
  364. continue;
  365. }
  366. $price += $priceThreshold['value'] * ($distance - $distanceThresholdTotal);
  367. break;
  368. }
  369. return $price;
  370. }
  371. /**
  372. * Get Last Driver Price from history Query Builder
  373. * @param $bookingId
  374. */
  375. private function lastDriverPriceQB($bookingId) {
  376. $em = $this->entityManager;
  377. $qb = $em->getRepository(BookingHistory::class)
  378. ->createQueryBuilder('h')
  379. ->select('h.payload')
  380. ->where('h.booking = :booking')
  381. ->andWhere("h.payload like '%driverPrice%'")
  382. ->setParameter('booking', $bookingId);
  383. return $qb;
  384. }
  385. /**
  386. * Get Last Driver Price from history
  387. * @param $bookingId
  388. */
  389. public function lastDriverPrice($bookingId) {
  390. $driverPrice = null;
  391. $em = $this->entityManager;
  392. $qb = $this->lastDriverPriceQB($bookingId)
  393. ->orderBy('h.date', 'DESC')
  394. ->setMaxResults(1);
  395. // Last Driver Price change from history.
  396. $payload = $qb->getQuery()->getOneOrNullResult();
  397. if (!$payload) {
  398. $expr = $em->getExpressionBuilder();
  399. $sub = $em->getRepository(User::class)
  400. ->createQueryBuilder('u')
  401. ->select('u.id')
  402. ->where("u.roles like '%OPERATOR%'")
  403. ->orWhere("u.roles like '%ADMIN%'");
  404. $qb = $this->lastDriverPriceQB($bookingId)
  405. ->andWhere(
  406. $expr->in('h.user', $sub->getDQL())
  407. )
  408. ->orderBy('h.date', 'DESC')
  409. ->setMaxResults(1);
  410. // Last OP/Admin Driver Price change from history.
  411. $payload = $qb->getQuery()->getOneOrNullResult();
  412. if (!$payload) {
  413. $qb = $this->lastDriverPriceQB($bookingId)
  414. ->orderBy('h.date', 'ASC')
  415. ->setMaxResults(1);
  416. // First Driver Price from history.
  417. $payload = $qb->getQuery()->getOneOrNullResult();
  418. }
  419. }
  420. if ($payload) {
  421. $payload = current($payload);
  422. if (!is_array($payload)) {
  423. $payload = unserialize($payload);
  424. }
  425. if (isset($payload['priceDetails'])) {
  426. $payload = $payload['priceDetails'];
  427. }
  428. $driverPrice = $payload['driverPrice'];
  429. if (is_array($driverPrice)) {
  430. $driverPrice = $driverPrice[0];
  431. }
  432. }
  433. if (!$driverPrice) {
  434. // Current Driver Price.
  435. $booking = $em->getRepository(Booking::class)->find($bookingId);
  436. if ($booking) {
  437. $driverPrice = $booking->getDriverPrice();
  438. }
  439. }
  440. return $driverPrice;
  441. }
  442. /**
  443. * ADD BOOKinG HISTORY
  444. * @param $booking
  445. * @param $actionType
  446. * @param $user
  447. * @param $payload
  448. */
  449. public function addBookingHistory($booking, $actionType, $user, $payload)
  450. {
  451. $bookingHistory = new BookingHistory();
  452. $bookingHistory->setBooking($booking);
  453. $bookingHistory->setActionType($actionType);
  454. $bookingHistory->setUser($user);
  455. $bookingHistory->setPayload($payload);
  456. $this->entityManager->persist($bookingHistory);
  457. $this->entityManager->flush();
  458. }
  459. /**
  460. * ADD BOOKinG HISTORY
  461. * @param Payment $payment
  462. * @param $amount
  463. * @param $type
  464. * @param $transactionId
  465. */
  466. public function addPaymentTransaction($payment, $amount, $type, $transactionId)
  467. {
  468. $settingRepository = $this->entityManager->getRepository(Settings::class);
  469. $this->entityManager->refresh($payment);
  470. $paymentTransaction = new Transaction();
  471. $paymentTransaction->setPayment($payment)
  472. ->setAmount($settingRepository->applyRoundTwoDecimal($amount))
  473. ->setType($type)
  474. ->setCreatedByUser($payment->getBooking()->getClientUser());
  475. switch ($type) {
  476. case Transaction::TYPE_BRAINTREE:
  477. $paymentTransaction->setBraintreeTransactionId($transactionId);
  478. break;
  479. case Transaction::TYPE_STRIPE:
  480. $paymentTransaction->setStripeTransactionId($transactionId);
  481. break;
  482. }
  483. $this->entityManager->persist($paymentTransaction);
  484. $payment->addTransaction($paymentTransaction);
  485. $payment->updateAmountLeft();
  486. $this->entityManager->persist($payment);
  487. $this->entityManager->flush();
  488. }
  489. }