<?php
namespace App\Controller\Api;
use App\Entity\model\Conveyance;
use App\Entity\Price;
use App\Entity\StopExt;
use App\Repository\PriceRepository;
use App\Repository\StopExtRepository;
use App\Service\Client\ClientRepository;
use App\Service\Client\IClient;
use App\Service\Client\Trip;
use App\Service\CurrencyService;
use App\Service\LabelBuilder;
use App\Service\PriceService;
use App\Service\UrlRewrite;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
/**
* @Route("/api", options={"i18n"=false})
*/
class ApiPriceController extends AbstractController {
private UrlRewrite $rewriteService;
private LabelBuilder $labelBuilder;
private ClientRepository $clientRepo;
private PriceService $priceService;
private CurrencyService $currencyService;
/** @required */
public function setServices(UrlRewrite $rewriteService, LabelBuilder $labelBuilder, ClientRepository $clientRepo, PriceService $priceService, CurrencyService $currencyService) {
$this->rewriteService = $rewriteService;
$this->labelBuilder = $labelBuilder;
$this->clientRepo = $clientRepo;
$this->priceService = $priceService;
$this->currencyService = $currencyService;
}
/**
* @Route("/prices", name="api_price_company")
* Recupere les prix du webservice ou depuis la bdd si deja existant
* 1-Les prix sont-ils en bdd? Oui=Fin
* 2-Récupération des prix depuis le scraper
* 3-Mise en bdd des prix
* 4-Return prix
* TODO delete level of outbound / inbound? requirements = {"companyId" = "1|2|3"}
* TODO send the response and after put in db ($response->send();)
* @ParamConverter("companyId", converter="querystring")
* @ParamConverter("stopExtDep", converter="querystring")
* @ParamConverter("stopExtArr", converter="querystring")
* @ParamConverter("stopIdDep", converter="querystring")
* @ParamConverter("stopIdArr", converter="querystring")
* @ParamConverter("dateFrom", converter="querystring")
* @ParamConverter("dateTo", converter="querystring")
* @ParamConverter("currency", converter="querystring")
* @ParamConverter("k", converter="querystring")
* @ParamConverter("forcenocache", converter="querystring")
* @ParamConverter("locale", converter="querystring")
*/
public function pricesByCompanyAction(int $companyId, string $stopExtDep, string $stopExtArr, int $stopIdDep, int $stopIdArr, string $dateFrom, ?string $dateTo = null, string $currency, string $k = null, ?bool $forcenocache = null, string $locale,
PriceRepository $priceRepo, StopExtRepository $stopExtRepo, EntityManagerInterface $em) {
$isOneWay = $dateTo == null;
$scraper = $this->clientRepo->find($companyId);
//dirty code to backlist wrong use of the api - GET /api/prices?companyId=13&stopExtDep=2015&stopExtArr=1965&stopIdDep=6&stopIdArr=10&dateFrom=2018-11-08&dateTo=¤cy=EUR
if (empty($k)) {
return $this->render("Default/error_api.html.twig");
}
//if the api is misused got cache price
$illegaluse = $k !== 'cbapp_PaidApi' && $k !== 'apicb';
$depStopExt = $stopExtRepo->findOneByCodeAndCompanyIdAndStopId($stopExtDep, $companyId, $stopIdDep);
$arrStopExt = $stopExtRepo->findOneByCodeAndCompanyIdAndStopId($stopExtArr, $companyId, $stopIdArr);
//if our IP is misused, and the depStopId / arrStopId are wrong
if ($depStopExt === null || $arrStopExt === null) {
$depStopExt = $stopExtRepo->findOneByCodeAndCompanyId($stopExtDep, $companyId);
$arrStopExt = $stopExtRepo->findOneByCodeAndCompanyId($stopExtArr, $companyId);
}
//stopExt not found because for example none (eg BBC)
if ($depStopExt === null || $arrStopExt === null) {
$depStopExt = StopExt::withParms($stopExtDep, $companyId, $stopIdDep);
$arrStopExt = StopExt::withParms($stopExtArr, $companyId, $stopIdArr);
}
$cached = !$forcenocache && $scraper->isCached();
$response = new ApiPriceResponse();
$response->debug = 'data_dbcache';
if ($illegaluse) { //only dislay cache price
$response->outbound = $this->priceService->getDbPrice($depStopExt, $arrStopExt, $dateFrom, 24 * 10);
$response->inbound = [];
$cached = true;
$response->debug = 'data_fresh_';
if ($response->outbound == null) $response->outbound = [];
foreach ($response->outbound as $p) {
$p->setCreatedAt(new DateTime());
}
} else if ($cached) {
$expiration = self::getExpirationHours($scraper->getCacheHours(), $dateFrom);
$response->outbound = $this->priceService->getDbPrice($depStopExt, $arrStopExt, $dateFrom, $expiration);
if (!$isOneWay) {
$expiration = self::getExpirationHours($scraper->getCacheHours(), $dateTo);
$response->inbound = $this->priceService->getDbPrice($arrStopExt, $depStopExt, $dateTo, $expiration);
}
}
// Scrapper will be run if no inbound or outbound price but no item this day (item empty storing that we already fetch that day but there is no trip
if (!$cached || $response->outbound === null || $response->inbound === null) {
$response->debug = 'data_fresh';
// $this->get('logger')->info("fetch from scrapper");
$trips = $scraper->getPrice($stopExtDep, $stopExtArr, $dateFrom, $dateTo);
if ($cached) {
//delete old price. Handle multiday and one day
$priceRepo->deletePriceByStopExt($depStopExt, $arrStopExt, $dateFrom);
if (!$isOneWay) {
$priceRepo->deletePriceByStopExt($arrStopExt, $depStopExt, $dateTo);
}
}
$response->outbound = []; //because it's out OR in not empty
$response->inbound = [];
//met en bdd
foreach ($trips as $t) {
$p = Price::createPriceFromTrip($depStopExt, $arrStopExt, $t);
PriceService::useReference($p, $em);
if ($cached) {
$em->persist($p);
}
//Add to outbound if outbound and date == $from/dep
if ($t->getDirection() == Trip::OUTBOUND) { //FIXME sometime direction is null (no ===), replace with boolean?
$response->outbound[] = $p;
}
//Add to outbound if inbound and date == $to/dep
if ($t->getDirection() == Trip::INBOUND) {
$response->inbound[] = $p;
}
}
//store that price has been scrapped but no trip that day
if ($cached && count($response->outbound) === 0) {
$p = Price::createNoTrip($depStopExt, $arrStopExt, $dateFrom);
PriceService::useReference($p, $em);
$em->persist($p);
}
if (!$isOneWay && count($response->inbound) == 0 && $cached) {
$p = Price::createNoTrip($arrStopExt, $depStopExt, $dateTo);
PriceService::useReference($p, $em);
$em->persist($p);
}
$em->flush();
$this->fillStopNameList($response->outbound, $depStopExt, $arrStopExt);
$this->fillStopNameList($response->inbound, $arrStopExt, $depStopExt);
}
//cors = true : is also called from www.cb.com -> demo.cb.com
return $this->responseJsonFromApiResponse($response, $currency, true, $scraper, $locale);
}
/**
* The cache is changed depending of the proximity of the trip
*/
public static function getExpirationHours(int $normalExpirationHours, string $date, \DateTime $today = null) : float {
$today = $today??new DateTime();
$today->setTime(0, 0, 0);
$match_date = DateTime::createFromFormat('Y-m-d', $date);
$match_date->setTime(0, 0, 0);
$diff = $today->diff($match_date);
$diffDays = (integer)$diff->format('%R%a'); // days before departure
switch ($diffDays) {
case -1: //maybe can happen if searching in America, server time is already the day after
case 0: //today
return $normalExpirationHours / 8;
case 1: //tomorrow
return $normalExpirationHours / 4; // 2hrs
case 2: //2 days
return $normalExpirationHours / 2; // 4hrs
}
return $normalExpirationHours;
}
protected function responseJsonFromApiResponse(ApiPriceResponse $res, string $currency, bool $cors, IClient $scraper, string $locale) {
$header = [];
if ($cors) {
$header['Access-Control-Allow-Origin'] = 'https://www.comparabus.com';
}
return new JsonResponse([
'debug' => $res->debug,
'inbound' => $this->convertPricesToApi($res->inbound, $currency, $scraper, $locale),
'outbound' => $this->convertPricesToApi($res->outbound, $currency, $scraper, $locale),
], 200, $header);
}
protected function convertPricesToApi(array $prices, string $currency, IClient $scraper, string $locale) {
$apiPrices = [];
foreach ($prices as $p)
$apiPrices[] = $this->convertPriceToApi($p, $currency, $scraper, $locale);
return $apiPrices;
}
protected function convertPriceToApi(Price $p, string $toCurrency, IClient $company, string $locale): array {
//takes type from Price otherwise takes type from Company
//TODO type must always be fixed
$conveyance = $p->getType() === null ? $company->getType() : Conveyance::fromPriceType($p->getType());
// if ($conveyance == Conveyance::FLIGHT) {
// $infos = $p->getInfo();
// foreach ($infos as &$info) {
// $cents = $this->currencyService->updateCurrencyInfo($info['cents'], $p->getCurrency(), $toCurrency);
// $info['price'] = CurrencyService::nicePrice($cents, $toCurrency, $locale);
// }
// $p->setInfo($infos);
// }
if ($p->getCents() > 0) {
$this->currencyService->updateCurrency($p, $toCurrency);
}
return [
'createdAt' => $p->getCreatedAt()->format('Y-m-d H:i:s'),
'companyId' => $p->getCompanyId(),
'companyName' => $company->getName(),
'type' => $conveyance,
'stopExtDep' => $p->getStopExtDep(),
'stopExtArr' => $p->getStopExtArr(),
'cents' => $p->getCents(),
'currency' => $p->getCurrency(),
'price' => $p->getCents() ? CurrencyService::nicePrice($p->getCents(), $p->getCurrency(), $locale) : null,
'duration' => $p->getDuration(),
'depDatetime' => $p->getDepDatetime()->format('Y-m-d H:i:s'),
'arrDatetime' => $p->getArrDatetime()->format('Y-m-d H:i:s'),
'link' => $p->getLink(),
'full' => $p->getFull(),
'stopNameDep' => $p->getStopNameDep(),
'stopNameArr' => $p->getStopNameArr(),
'carrierName' => $p->getCarrierName(),
'carrierCode' => $p->getCarrierCode(),
'connection' => $p->getConnection(),
'id' => $p->getId(),
'stopIdDep' => $p->getDepStop()->getId(),
'stopIdArr' => $p->getArrStop()->getId(),
'info' => $p->getInfo()
];
}
/**
* @param Price[] $prices
*/
private function fillStopNameList(array $prices, StopExt $depStop, StopExt $arrStop): void {
foreach ($prices as $p) {
$p->fillStopNameIfEmpty($depStop, $arrStop);
}
}
}