<?php
namespace App\Controller;
use App\Entity\Brand;
use App\Entity\Company;
use App\Entity\Photo;
use App\Entity\Product;
use App\Entity\Project;
use App\Entity\Record;
use App\Entity\Shop;
use App\Message\ImportFileMessage;
use App\Service\FileService;
use App\Service\TokenService;
use App\Service\ZpriceHelper;
use Aws\S3\Exception\S3Exception;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\Persistence\ManagerRegistry;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface;
class ApiController extends AbstractController
{
public const PHOTO_PRICE_TYPE = 1;
public const PHOTO_PRODUCT_TYPE = 2;
public const PHOTO_OTHER_TYPE = 3;
/**
* @Route("/api/updateCatalog", methods={"GET"})
*
* @throws JWTDecodeFailureException
*/
public function updateCatalogAction(
ManagerRegistry $doctrine,
Request $request,
ZpriceHelper $zpriceHelper,
SerializerInterface $serializer,
TokenService $tokenService,
): Response {
set_time_limit(0);
ini_set('memory_limit', '1024M');
$zpriceHelper->roleChecker($doctrine, 'ROLE_RELEVEUR');
$project = $tokenService->getProjectFromToken();
$manager = $doctrine->getManager();
$query = $manager->createQuery(
"SELECT p.name as articleName, b.name as brandName, p.ean, p.photo as image, p.photomini as imagemini
FROM App\Entity\Product as p
JOIN p.brand b
WHERE p.project = :project
AND p.inCatalog = true"
);
$query->setParameter('project', $project);
$jsons = [];
$jsons[] = '"products":'.$serializer->serialize($query->getResult(), 'json');
$finalJson = '{'.implode(',', $jsons).'}';
$publicCsvDir = 'tempcsv'
.DIRECTORY_SEPARATOR.md5($this->getUser()->getUserIdentifier().'zmpriceseeddir');
$tempCsvDir = $this->getParameter('kernel.project_dir')
.DIRECTORY_SEPARATOR.'public'
.DIRECTORY_SEPARATOR.$publicCsvDir;
$filename = md5($project->getName().time().'zmpriceseedfilename').'.json';
if (!is_dir($tempCsvDir)) {
mkdir($tempCsvDir, 0777, true);
} else {
if (file_exists($tempCsvDir.DIRECTORY_SEPARATOR.$filename)) {
unlink($tempCsvDir.DIRECTORY_SEPARATOR.$filename);
}
}
file_put_contents($tempCsvDir.DIRECTORY_SEPARATOR.$filename, $finalJson);
return new JsonResponse([
'fileUrl' => $request->getSchemeAndHttpHost().'/'.$publicCsvDir.'/'.$filename,
'contentLength' => strlen($finalJson),
]);
}
/**
* @throws \Exception
*
* @Route("api/pushRecord", methods={"POST"})
*/
public function pushRecordAction(
ManagerRegistry $doctrine,
Request $request,
ZpriceHelper $zpriceHelper,
KernelInterface $kernel,
): JsonResponse {
$zpriceHelper->roleChecker($doctrine, 'ROLE_RELEVEUR');
$content = json_decode($request->getContent());
$contentHash = md5($request->getContent());
if (!$content) {
throw $this->createNotFoundException('Unexpected data');
}
if (!empty($content->projectId)) {
$project = $doctrine->getRepository(Project::class)->find($content->projectId);
}
if (empty($project)) {
throw $this->createNotFoundException('Project not found');
}
$existingRecord = $doctrine->getRepository(Record::class)->findOneBy(['contentHash' => $contentHash]);
if ($existingRecord) {
return new JsonResponse(['success' => 'record already synced']);
}
if (!empty($content->shop->placeId)) {
$shop = $doctrine->getRepository(Shop::class)->findOneBy([
'placeId' => 'places/'.$content->shop->placeId]);
if (!$shop) {
$shop = new Shop();
$company = $doctrine->getRepository(Company::class)->findOneBy(['name' => $content->shop->company]);
if (!$company) {
throw $this->createNotFoundException('Company not found');
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://places.googleapis.com/v1/places/'
.$content->shop->placeId,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Goog-Api-Key: '.$_ENV['GOOGLE_PLACES_API_KEY'],
'X-Goog-FieldMask: name,formattedAddress,addressComponents,location,displayName,types',
],
]);
$response = curl_exec($curl);
curl_close($curl);
$gApiResponse = json_decode($response);
$shop->setCompany($company)
->setName($gApiResponse->displayName->text)
->setCity($this->getGApiAddressInfo($gApiResponse, 'locality'))
->setPostcode($this->getGApiAddressInfo($gApiResponse, 'postal_code'))
->setAddress($gApiResponse->formattedAddress)
->setLatitude($gApiResponse->location->latitude)
->setLongitude($gApiResponse->location->longitude)
->setSecteur(implode(',', $gApiResponse->types))
->setPlaceId($gApiResponse->name);
$doctrine->getManager()->persist($shop);
}
} else {
throw $this->createNotFoundException('Shop details missing');
}
if (!empty($content->product->ean)) {
$product = $doctrine
->getRepository(Product::class)
->findOneBy(['ean' => $content->product->ean, 'project' => $project->getId()]);
if (!$product) {
$zpriceHelper->roleChecker($doctrine, 'ROLE_RELEVEUR');
$product = new Product();
$product->setEan($content->product->ean)
->setProject($project)
->setInCatalog(false)
->setLastUpdate(new \DateTime('now'));
if (!empty($content->product->brand)) {
$brand = $doctrine->getRepository(Brand::class)->find($content->product->brand);
if (!$brand) {
$brand = $doctrine
->getRepository(Brand::class)
->findOneBy(['name' => $content->product->brand]);
}
} else {
$brand = $doctrine->getRepository(Brand::class)->findOneBy(['name' => 'Inconnue']);
if (!$brand) {
$brand = new Brand();
$brand->setName('Inconnue');
$brand->setLogo('');
}
}
$product->setBrand($brand);
if (!empty($content->product->name)) {
$product->setName($content->product->name);
}
}
} else {
throw $this->createNotFoundException('EAN missing');
}
$record = new Record();
$record->setProduct($product)
->setRecordTime(\DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $content->recordTime))
->setSyncTime(new \DateTimeImmutable('now'))
->setPrice($content->price)
->setShop($shop)
->setContentHash($contentHash)
->setUser($this->getUser());
if (!empty($content->photos)) {
foreach ($content->photos as $photo) {
try {
$storedFile = $zpriceHelper->storePhoto($photo);
$directory = $storedFile['directory'];
$filename = $storedFile['filename'];
$photoUrl = $storedFile['url'];
if ($photoUrl) {
$photoObj = new Photo();
$photoObj->setFilename($directory.'/'.$filename);
$photoObj->setPublicurl($photoUrl);
$photoObj->setType($photo->type);
$photoObj->setStockage('s3');
switch ($photo->type) {
case self::PHOTO_PRODUCT_TYPE:
$product->setPhoto($photoUrl);
break;
default:
$record->addPhoto($photoObj);
break;
}
}
} catch (S3Exception $e) {
throw new \Exception('Image storage Exception : '.$e->getMessage());
}
}
}
$manager = $doctrine->getManager();
$manager->persist($record);
$manager->flush();
return new JsonResponse(['success' => 'record sync successful']);
}
public function getGApiAddressInfo($gApiResponse, $type): string
{
foreach ($gApiResponse->addressComponents as $val) {
if (in_array($type, $val->types)) {
return $val->longText;
}
}
return '';
}
/**
* @throws NoResultException
* @throws NonUniqueResultException
* @throws TransportExceptionInterface|JWTDecodeFailureException
*
* @Route("/api/exportCSV", methods={"GET"})
*/
public function exportCSVAction(
Request $request,
EntityManagerInterface $entityManager,
ZpriceHelper $zpriceHelper,
MailerInterface $mailer,
): Response {
$user = $this->getUser();
$dateStartStr = $request->query->get('dateStart');
$dateEndStr = $request->query->get('dateEnd');
$dateStart = \DateTime::createFromFormat('Y-m-d H:i:s', $dateStartStr.' 00:00:00');
$dateEnd = \DateTime::createFromFormat('Y-m-d H:i:s', $dateEndStr.' 23:59:59');
$query = $entityManager
->createQuery("
SELECT
r.id,
u.email,
p.ean,
r.recordTime,
s.name as shopname,
s.city as city,
s.postcode as postcode,
r.price,
ph.publicurl as photo,
p.name,
b.name as brand
FROM App\Entity\Record r
JOIN r.shop s
JOIN r.product p
JOIN r.photos ph
JOIN r.user u
JOIN p.brand b
WHERE p.project = :projectId
AND r.recordTime >= :dateStart
AND r.recordTime <= :dateEnd
AND r.user = :user
AND ph.type = 1
")
->setParameter('projectId', $zpriceHelper->getCurrentProject())
->setParameter('dateStart', $dateStart)
->setParameter('dateEnd', $dateEnd)
->setParameter('user', $user);
$records = $query->getResult();
$tempCsvDir = $this->getParameter('kernel.project_dir').DIRECTORY_SEPARATOR
.'var'.DIRECTORY_SEPARATOR.'tempcsv';
if (!is_dir($tempCsvDir)) {
mkdir($tempCsvDir);
}
$now = new \DateTime('now');
$filename = 'export-u'.$user->getId().'-'.$now->format('hmsdmy').'.csv';
$csvHandle = fopen($tempCsvDir.DIRECTORY_SEPARATOR.$filename, 'w');
$headerList = [
'RECORD_ID',
'EAN',
'PRODUCT_NAME',
'VARIATION_NAME',
'BRAND',
'PRICE',
'SHOP',
'CITY',
'POSTCODE',
'DATE',
'EMAIL',
'RECORD_IMG',
];
fputcsv($csvHandle, $headerList, ';');
foreach ($records as $record) {
$names = explode(' | ', $record['name']);
$values = [];
$values[] = $record['id'];
$values[] = $record['ean'];
$values[] = $names[0];
$values[] = count($names) > 1 ? $names[1] : '';
$values[] = $record['brand'];
$values[] = $this->formatEuro($record['price']);
$values[] = $record['shopname'];
$values[] = $record['city'];
$values[] = $record['postcode'];
$values[] = $record['recordTime']->format('d-m-Y');
$values[] = $record['email'];
$values[] = $record['photo'];
fputcsv($csvHandle, $values, ';');
}
fclose($csvHandle);
$queryRecordedShop = $entityManager
->createQuery("
SELECT COUNT(DISTINCT s.id) as shopCount
FROM App\Entity\Record r
JOIN r.shop s
JOIN r.product p
JOIN r.photos ph
JOIN r.user u
JOIN p.brand b
WHERE p.project = :projectId
AND r.recordTime >= :dateStart
AND r.recordTime <= :dateEnd
AND r.user = :user
AND ph.type = 1
")
->setParameter('projectId', $zpriceHelper->getCurrentProject())
->setParameter('dateStart', $dateStart)
->setParameter('dateEnd', $dateEnd)
->setParameter('user', $user);
$shopCount = $queryRecordedShop->getSingleScalarResult();
$email = (new Email())
->from('[email protected]')
->to($user->getUserIdentifier())
->subject('ZPriceAuditor: Relevés du '.$dateStart->format('d/m/Y').' au '.$dateEnd->format('d/m/Y'))
->html(
'<h2>Votre relevé</h2>Vous trouverez en pièce jointe votre relevé pour la période du '
.$dateStart->format('d/m/Y').' au '.$dateEnd->format('d/m/Y').' contenant '
.count($records)." relevés pour $shopCount magasins"
)
->attachFromPath($tempCsvDir.DIRECTORY_SEPARATOR.$filename);
$mailer->send($email);
unlink($tempCsvDir.DIRECTORY_SEPARATOR.$filename);
return new JsonResponse(['records' => count($records), 'recordedShops' => $shopCount]);
}
/**
* @Route("api/pushCatalog")
*
* @throws \Exception
*/
public function pushCatalogAction(
Request $request,
ZpriceHelper $zpriceHelper,
ManagerRegistry $doctrine,
MessageBusInterface $messageBus,
FileService $fileService,
): JsonResponse {
$zpriceHelper->roleChecker($doctrine, 'ROLE_ADMIN');
$fileService->tempcsvPurge();
$catalog = $request->get('catalog');
if (!empty($catalog)) {
$tempdir = $this->getParameter('kernel.project_dir').
DIRECTORY_SEPARATOR.'var'.DIRECTORY_SEPARATOR.'tempcsv';
if (!is_dir($tempdir)) {
mkdir($tempdir, 0777, true);
}
$filename = rand(300000000, 900000000000).'.csv';
$filepath = $tempdir.DIRECTORY_SEPARATOR.$filename;
file_put_contents($filepath, $catalog);
$msgBusReturn = $messageBus->dispatch(
new ImportFileMessage(
$filepath,
$zpriceHelper->getCurrentProject()->getId()
)
);
/*
* @todo notif de complétion ou d'erreur ?
*/
return new JsonResponse(['success' => 'Import will run in background -- '.$filepath]);
} else {
throw new NotAcceptableHttpException('Missing datas');
}
}
/**
* @Route("api/getPlaces")
*
* @throws JWTDecodeFailureException
*/
public function getPlaces(
ManagerRegistry $doctrine,
Request $request,
ZpriceHelper $zpriceHelper,
KernelInterface $kernel,
): JsonResponse {
$zpriceHelper->roleChecker($doctrine, 'ROLE_RELEVEUR');
$company = $request->query->get('company');
$latitude = $request->query->get('latitude');
$longitude = $request->query->get('longitude');
if (empty($company) || empty($latitude) || empty($longitude)) {
throw $this->createAccessDeniedException();
}
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => 'https://places.googleapis.com/v1/places:searchText',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => '{
"textQuery": "'.$company.'",
"languageCode": "fr",
"locationBias": {
"circle" : {
"center" : {
"latitude": "'.$latitude.'",
"longitude": "'.$longitude.'"
}
}
}
}',
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Goog-Api-Key: '.$_ENV['GOOGLE_PLACES_API_KEY'],
'X-Goog-FieldMask: places.id,places.displayName,places.formattedAddress,places.location',
],
]);
$response = json_decode(curl_exec($curl), true);
foreach ($response['places'] as $key => $place) {
$place['distance'] = number_format($zpriceHelper->getDistance(
$place['location']['latitude'],
$place['location']['longitude'],
$latitude,
$longitude
), 2);
$response['places'][$key] = $place;
if ($place['distance'] > 30) {
unset($response['places'][$key]);
}
}
usort($response['places'], function ($a, $b) {
return $a['distance'] <=> $b['distance'];
});
return new JsonResponse($response);
}
public function formatEuro(float $amount): string
{
$formatter = new \NumberFormatter('fr_FR', \NumberFormatter::CURRENCY);
return $formatter->formatCurrency($amount, 'EUR');
}
}