WIP: Implement closer BPN integration

This commit is contained in:
Björn Fromme
2023-10-06 18:14:05 +02:00
parent bea5edd615
commit 25a90b04aa
23 changed files with 628 additions and 224 deletions
-1
View File
@@ -16,7 +16,6 @@ class ApiClient
{
public const TYPE_NOTIFICATION = 'HINWEIS';
public const TYPE_CUSTOMER_DATA = 'KUNDENKONTO';
public const TYPE_PRODUCTS = 'PRODUKTE';
public const TYPE_BASE_DATA_COUNTRIES = 'STAMMLAENDER';
public const TYPE_BASE_DATA_PICKUPS = 'STAMMZUSTIEGE';
public const TYPE_BASE_DATA_HOTELS = 'STAMMHOTELS';
@@ -1,39 +0,0 @@
<?php
namespace App\BusProNet\DataProvider;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
use App\BusProNet\Model\Product;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class ProductDataProvider
{
public function __construct(private readonly ApiClient $apiClient, private readonly CacheInterface $cache)
{}
public function getAll(): array
{
try {
$products = $this->cache->get('bpn_products', function (ItemInterface $item) {
$item->expiresAfter(3600);
return $this
->apiClient
->getBaseData(ApiClient::TYPE_PRODUCTS)
->getItems()
;
});
} catch (ApiClientException $e) {
$products = [];
}
return $products;
}
public function get(int $busProId): ?Product
{
return $this->getAll()[$busProId] ?? null;
}
}
-46
View File
@@ -1,46 +0,0 @@
<?php
namespace App\BusProNet\Model;
class Product
{
private ?int $id = null;
private ?string $code = null;
private ?string $name = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(?int $id): static
{
$this->id = $id;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(?string $code): static
{
$this->code = $code;
return $this;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): static
{
$this->name = $name;
return $this;
}
}
-25
View File
@@ -12,7 +12,6 @@ use App\BusProNet\Model\CrmAttributesResponse;
use App\BusProNet\Model\Hotel;
use App\BusProNet\Model\NotificationResponse;
use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Product;
use App\BusProNet\Model\ProfileResponse;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -59,8 +58,6 @@ class ResponseParser
return $this->createPickupsResponse($xml);
case ApiClient::TYPE_BASE_DATA_HOTELS:
return $this->createHotelsResponse($xml);
case ApiClient::TYPE_PRODUCTS:
return $this->createProductsResponse($xml);
}
throw new ResponseParserException('Unable to parse XML response');
@@ -241,28 +238,6 @@ class ResponseParser
return new BaseDataResponse($hotels);
}
public function createProductsResponse(\SimpleXMLElement $xml): BaseDataResponse
{
$products = [];
foreach ($xml->xpath('produkte/produkt') as $item) {
$code = (string) $item->attributes()['code'];
if (true === empty($code)) {
continue;
}
$id = (int) $item->attributes()['id'];
$product = new Product();
$product
->setId($id)
->setName((string) $item->attributes()['bezeichnung'])
->setCode($code)
;
$products[$id] = $product;
}
return new BaseDataResponse($products);
}
private function resolveOptions(array $options): array
{
$optionsResolver = new OptionsResolver();
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace App\Command;
use App\BusProNet\DataProvider\HotelDataProvider;
use App\BusProNet\DataProvider\PickupDataProvider;
use Doctrine\DBAL\Connection;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Finder\Finder;
#[AsCommand(
name: 'app:bpn-import',
description: 'Imports dates, products and hotels from BusProNet',
)]
class BpnImportCommand extends Command
{
public function __construct(
private readonly Connection $connection,
private readonly HotelDataProvider $hotelDataProvider,
private readonly PickupDataProvider $pickupDataProvider,
private readonly string $xmlFilesPath
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
// Get all xml files
$xmlFiles = (new Finder())
->name('Ziel_*.xml')
->in($this->xmlFilesPath)
;
$totalCount = count($xmlFiles);
if (0 === $totalCount) {
$io->error('No XML files available or found');
return Command::FAILURE;
}
// Flush dates
$this
->connection
->executeQuery('TRUNCATE TABLE bpn_date')
;
$io->info('Found '.$totalCount.' XML files');
$datesCount = 0;
// Iterate over xml files
foreach ($xmlFiles as $xmlFile) {
// Load xml file into simplexml object
$xml = simplexml_load_file($xmlFile);
// Iterate over all dates
foreach ($xml->xpath('reise/termin') as $dateXml) {
$product = (string) $dateXml->xpath('text')[0];
$busProId = (int) $dateXml->attributes()['idbuspro'];
$code = (string) $dateXml->attributes()['code'];
$dateFrom = \DateTimeImmutable::createFromFormat('d.m.Y', (string) $dateXml->attributes()['termin']);
$dateTo = \DateTimeImmutable::createFromFormat('d.m.Y', (string) $dateXml->attributes()['bis']);
$bus = false;
// Iterate over all service entries and look for 'bus'
foreach ($dateXml->xpath('lei_befoerderung/leistung') as $serviceXml) {
if ('BUS' === (string) $serviceXml->attributes()['unterart']) {
$bus = true;
break;
}
}
// Find pickups
$datePickups = [];
foreach ($dateXml->xpath('zustiege/zustieg') as $pickupXml) {
$pickupBusProId = (int) $pickupXml->attributes()['idbuspro'];
if ($pickupBusProId) {
$pickup = $this->pickupDataProvider->get($pickupBusProId);
$datePickups[] = [
'busProId' => $pickupBusProId,
'city' => $pickup->getCity(),
'street' => $pickup->getStreet(),
'time' => (string) $pickupXml->attributes()['zeit'],
];
}
}
// Iterate over all hotel entries
foreach ($dateXml->xpath('hotel') as $hotelXml) {
$hotelBusProId = (int) $hotelXml->attributes()['idbuspro'];
$hotel = $this->hotelDataProvider->get($hotelBusProId);
// Finally store collected data
$this
->connection
->insert('bpn_date', [
'code' => $code,
'bus_pro_id' => $busProId,
'date_from' => $dateFrom->format('Y-m-d'),
'date_to' => $dateTo->format('Y-m-d'),
'product' => $product,
'hotel' => $hotel->getName(),
'hotel_code' => $hotel->getCode(),
'hotel_bus_pro_id' => $hotelBusProId,
'bus' => (int) $bus,
'pickups' => json_encode($datePickups),
])
;
++$datesCount;
}
}
}
$io->info('Imported '.$datesCount.' dates');
return Command::SUCCESS;
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Controller\Admin\Autocomplete;
use App\Repository\BpnDateRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class BpnDateController extends AbstractController
{
public function __construct(private readonly BpnDateRepository $bpnDateRepository)
{}
#[Route('/admin/autocomplete/bpndate', name: 'app_admin_autocomplete_bpndate')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): JsonResponse
{
try {
$query = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$queryString = $query['search'];
} catch (\JsonException $e) {
throw $this->createNotFoundException();
}
$dates = $this->bpnDateRepository->getAutocompletionData($queryString);
return $this->json($dates);
}
}
@@ -1,43 +0,0 @@
<?php
namespace App\Controller\Admin\Autocomplete;
use App\BusProNet\DataProvider\HotelDataProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class HotelController extends AbstractController
{
public function __construct(private readonly HotelDataProvider $hotelDataProvider)
{}
#[Route('/admin/autocomplete/hotel', name: 'app_admin_autocomplete_hotel')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): JsonResponse
{
try {
$query = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$queryString = $query['search'];
} catch (\JsonException $e) {
throw $this->createNotFoundException();
}
$hotels = $this->hotelDataProvider->getAll();
$data = [];
foreach ($hotels as $hotel) {
$name = $hotel->getName();
if (1 === preg_match('/'.preg_quote($queryString, '/').'/i', $name)) {
$data[] = [
'value' => $hotel->getBusProId(),
'text' => sprintf('%s (%s)', $name, $hotel->getCode()),
];
}
}
return $this->json($data);
}
}
@@ -1,43 +0,0 @@
<?php
namespace App\Controller\Admin\Autocomplete;
use App\BusProNet\DataProvider\ProductDataProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class ProductController extends AbstractController
{
public function __construct(private readonly ProductDataProvider $productDataProvider)
{}
#[Route('/admin/autocomplete/product', name: 'app_admin_autocomplete_product')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Request $request): JsonResponse
{
try {
$query = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
$queryString = $query['search'];
} catch (\JsonException $e) {
throw $this->createNotFoundException();
}
$products = $this->productDataProvider->getAll();
$data = [];
foreach ($products as $product) {
$name = $product->getName();
if (1 === preg_match('/'.preg_quote($queryString, '/').'/i', $name)) {
$data[] = [
'value' => $product->getId(),
'text' => sprintf('%s (%s)', $name, $product->getCode()),
];
}
}
return $this->json($data);
}
}
+6 -6
View File
@@ -27,9 +27,9 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ORM\Column(nullable: true)]
#[ORM\ManyToOne]
#[Assert\NotNull(message: 'Bitte gib die Destination an')]
private ?int $destinationBusProId = null;
private ?BpnDate $destination = null;
#[ORM\ManyToOne]
#[Assert\NotNull(message: 'Bitte gib das Jobprofil an')]
@@ -93,14 +93,14 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
return $this->uuid;
}
public function getDestinationBusProId(): ?int
public function getDestination(): ?BpnDate
{
return $this->destinationBusProId;
return $this->destination;
}
public function setDestinationBusProId(?int $destinationBusProId): static
public function setDestination(?BpnDate $destination): static
{
$this->destinationBusProId = $destinationBusProId;
$this->destination = $destination;
return $this;
}
+176
View File
@@ -0,0 +1,176 @@
<?php
namespace App\Entity;
use App\Repository\BpnDateRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BpnDateRepository::class)]
class BpnDate
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $busProId = null;
#[ORM\Column(length: 32)]
private ?string $code = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateFrom = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateTo = null;
#[ORM\Column]
private ?bool $bus;
#[ORM\Column(length: 255)]
private ?string $product = null;
#[ORM\Column(length: 255)]
private ?string $hotel = null;
#[ORM\Column(length: 32)]
private ?string $hotelCode = null;
#[ORM\Column]
private ?int $hotelBusProId = null;
#[ORM\Column]
private array $pickups = [];
public function __construct()
{
$this->bus = false;
}
public function getId(): ?int
{
return $this->id;
}
public function getBusProId(): ?int
{
return $this->busProId;
}
public function setBusProId(int $busProId): static
{
$this->busProId = $busProId;
return $this;
}
public function getDateFrom(): ?\DateTimeImmutable
{
return $this->dateFrom;
}
public function setDateFrom(\DateTimeImmutable $dateFrom): static
{
$this->dateFrom = $dateFrom;
return $this;
}
public function getDateTo(): ?\DateTimeImmutable
{
return $this->dateTo;
}
public function setDateTo(\DateTimeImmutable $dateTo): static
{
$this->dateTo = $dateTo;
return $this;
}
public function isBus(): ?bool
{
return $this->bus;
}
public function setBus(bool $bus): static
{
$this->bus = $bus;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
public function getProduct(): ?string
{
return $this->product;
}
public function setProduct(string $product): static
{
$this->product = $product;
return $this;
}
public function getHotel(): ?string
{
return $this->hotel;
}
public function setHotel(string $hotel): static
{
$this->hotel = $hotel;
return $this;
}
public function getHotelCode(): ?string
{
return $this->hotelCode;
}
public function setHotelCode(?string $hotelCode): static
{
$this->hotelCode = $hotelCode;
return $this;
}
public function getHotelBusProId(): ?int
{
return $this->hotelBusProId;
}
public function setHotelBusProId(?int $hotelBusProId): static
{
$this->hotelBusProId = $hotelBusProId;
return $this;
}
public function getPickups(): array
{
return $this->pickups;
}
public function setPickups(array $pickups): static
{
$this->pickups = $pickups;
return $this;
}
}
+5 -2
View File
@@ -3,6 +3,7 @@
namespace App\Form;
use App\Entity\Assignment;
use App\Entity\BpnDate;
use App\Entity\Fee;
use App\Entity\JobProfile;
use App\Entity\User;
@@ -25,9 +26,11 @@ class AssignmentType extends AbstractType
'label' => 'Anzahl',
'required' => false,
])
->add('destinationBusProId', AutocompleteChoiceType::class, [
->add('destination', AutocompleteEntityType::class, [
'label' => 'Destination',
'endpoint_route' => 'app_admin_autocomplete_hotel',
'class' => BpnDate::class,
'label_property' => 'product',
'endpoint_route' => 'app_admin_autocomplete_bpndate',
])
->add('jobProfile', EntityType::class, [
'label' => 'Jobprofil',
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Repository;
use App\Entity\BpnDate;
use App\Repository\Traits\QueryHelperTrait;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<BpnDate>
*
* @method BpnDate|null find($id, $lockMode = null, $lockVersion = null)
* @method BpnDate|null findOneBy(array $criteria, array $orderBy = null)
* @method BpnDate[] findAll()
* @method BpnDate[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class BpnDateRepository extends ServiceEntityRepository
{
use QueryHelperTrait;
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BpnDate::class);
}
public function getAutocompletionData(string $search): array
{
$qb = $this->createQueryBuilder('bpn_date');
$dates = $qb
->select('bpn_date.id', 'bpn_date.product', 'bpn_date.hotel', 'bpn_date.dateFrom', 'bpn_date.dateTo')
->where($qb->expr()->orX(
$qb->expr()->like('bpn_date.product', ':product'),
$qb->expr()->like('bpn_date.hotel', ':hotel')
))
->setParameter('product', '%'.$this->escapeLikeWildcards($search).'%')
->setParameter('hotel', '%'.$this->escapeLikeWildcards($search).'%')
->orderBy('bpn_date.product', 'ASC')
->addOrderBy('bpn_date.dateFrom', 'ASC')
->getQuery()
->getArrayResult()
;
$data = [
[
'value' => '',
'text' => '...',
],
];
foreach ($dates as $date) {
$data[] = [
'value' => $date['id'],
'text' => sprintf('%s (%s - %s)', $date['product'], $date['dateFrom']->format('d.m.y'), $date['dateTo']->format('d.m.y')),
];
}
return $data;
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Repository\Traits;
trait QueryHelperTrait
{
public function escapeLikeWildcards(string $value): string
{
return addcslashes($value, '_%');
}
}