feat: api endpoints and xml sync for contingents data
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\BusProNet\Utility\DateCodeUtility;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Service\ContingentDataService;
|
||||
use App\Service\TravelDataProvider;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* API endpoints for contingent availability data.
|
||||
*
|
||||
* References are accepted as either numeric IDs or business codes:
|
||||
* - `hotelRef`: hotel ID (`idbuspro`) or hotel code
|
||||
* - `dateRef`: date ID or date code (sanitized before mapping)
|
||||
*/
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class ContingentController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ContingentDataService $contingentDataService,
|
||||
private readonly TravelDataProvider $travelDataProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(
|
||||
'/contingents/calendar',
|
||||
name: 'api_contingents_calendar',
|
||||
methods: ['GET'],
|
||||
)]
|
||||
public function calendar(Request $request): JsonResponse
|
||||
{
|
||||
$hotelReference = $request->query->get('hotelRef');
|
||||
|
||||
if (null === $hotelReference || '' === trim($hotelReference)) {
|
||||
return $this->json(['error' => 'hotelRef is required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->handleCalendar($request, $hotelReference);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
'/contingents',
|
||||
name: 'api_contingents_single',
|
||||
methods: ['GET'],
|
||||
)]
|
||||
public function byDate(Request $request): JsonResponse
|
||||
{
|
||||
$hotelReference = $request->query->get('hotelRef');
|
||||
$dateReference = $request->query->get('dateRef');
|
||||
|
||||
if (null === $hotelReference || '' === trim($hotelReference)) {
|
||||
return $this->json(['error' => 'hotelRef is required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === $dateReference || '' === trim($dateReference)) {
|
||||
return $this->json(['error' => 'dateRef is required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->handleByDate($hotelReference, $dateReference);
|
||||
}
|
||||
|
||||
#[Route(
|
||||
'/contingents/rooms',
|
||||
name: 'api_contingents_rooms',
|
||||
methods: ['GET'],
|
||||
)]
|
||||
public function rooms(Request $request): JsonResponse
|
||||
{
|
||||
$hotelReference = $request->query->get('hotelRef');
|
||||
$dateReference = $request->query->get('dateRef');
|
||||
$dateFrom = $request->query->get('dateFrom');
|
||||
$dateTo = $request->query->get('dateTo');
|
||||
$myEpUrl = $request->query->get('my_ep_url');
|
||||
|
||||
if (null === $hotelReference || '' === trim($hotelReference)) {
|
||||
return $this->json(['error' => 'hotelRef is required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === $dateReference || '' === trim($dateReference)) {
|
||||
return $this->json(['error' => 'dateRef is required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
return $this->json(['error' => 'dateFrom and dateTo are required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->handleRooms(
|
||||
$hotelReference,
|
||||
$dateReference,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
is_string($myEpUrl) ? $myEpUrl : null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared execution path for calendar data after hotel reference validation.
|
||||
*/
|
||||
private function handleCalendar(Request $request, string $hotelReference): JsonResponse
|
||||
{
|
||||
$hotelId = $this->resolveHotelId($hotelReference);
|
||||
|
||||
if (null === $hotelId) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$dateFrom = $request->query->get('dateFrom');
|
||||
$dateTo = $request->query->get('dateTo');
|
||||
|
||||
if (null === $dateFrom || null === $dateTo) {
|
||||
return $this->json(['error' => 'dateFrom and dateTo are required'], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$events = $this->contingentDataService->getCalendarEvents($hotelId, $dateFrom, $dateTo);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->json($events, Response::HTTP_OK, [], ['groups' => ['api:contingent']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared execution path for daily contingent summary after reference validation.
|
||||
*/
|
||||
private function handleByDate(string $hotelReference, string $dateReference): JsonResponse
|
||||
{
|
||||
$hotelId = $this->resolveHotelId($hotelReference);
|
||||
$dateId = $this->resolveDateId($dateReference);
|
||||
|
||||
if (null === $hotelId || null === $dateId) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$contingents = $this->contingentDataService->getAvailableContingents($hotelId, $dateId);
|
||||
} catch (TravelNotFoundException|HotelNotInTravelException) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return $this->json($contingents, Response::HTTP_OK, [], ['groups' => ['api:contingent']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared execution path for room-level availability after reference validation.
|
||||
*/
|
||||
private function handleRooms(
|
||||
string $hotelReference,
|
||||
string $dateReference,
|
||||
string $dateFrom,
|
||||
string $dateTo,
|
||||
?string $myEpUrl = null,
|
||||
): JsonResponse {
|
||||
$hotelId = $this->resolveHotelId($hotelReference);
|
||||
$dateId = $this->resolveDateId($dateReference);
|
||||
|
||||
if (null === $hotelId || null === $dateId) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
try {
|
||||
$rooms = $this->contingentDataService->getAvailableRooms($dateFrom, $dateTo, $hotelId, $dateId, $myEpUrl);
|
||||
} catch (TravelNotFoundException|HotelNotInTravelException) {
|
||||
return $this->json(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->json(['error' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->json($rooms, Response::HTTP_OK, [], ['groups' => ['api:contingent']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a hotel reference to hotel ID.
|
||||
*
|
||||
* Numeric values are treated as IDs, otherwise mapped as hotel code.
|
||||
*/
|
||||
private function resolveHotelId(string $hotelReference): ?int
|
||||
{
|
||||
return $this->resolveReferenceId(
|
||||
$hotelReference,
|
||||
fn (string $reference): ?int => $this->travelDataProvider->mapHotelCodeToId($reference),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a date reference to date ID.
|
||||
*
|
||||
* Numeric values are treated as IDs, otherwise mapped as date code.
|
||||
* Date codes are sanitized first (uppercased and separators removed).
|
||||
*/
|
||||
private function resolveDateId(string $dateReference): ?int
|
||||
{
|
||||
return $this->resolveReferenceId(
|
||||
$dateReference,
|
||||
fn (string $reference): ?int => $this->travelDataProvider->mapDateCodeToId($reference),
|
||||
fn (string $reference): string => (new DateCodeUtility())->sanitize($reference),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic resolver for mixed ID/code references.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Trim and reject empty values.
|
||||
* 2. If numeric, return as integer ID.
|
||||
* 3. Optionally normalize the value.
|
||||
* 4. Map normalized code to ID.
|
||||
*
|
||||
* @param callable $mapper maps code input to ID
|
||||
* @param callable|null $normalizer optional code normalizer before mapping
|
||||
*/
|
||||
private function resolveReferenceId(
|
||||
string $reference,
|
||||
callable $mapper,
|
||||
?callable $normalizer = null,
|
||||
): ?int {
|
||||
$trimmedReference = trim($reference);
|
||||
|
||||
if ('' === $trimmedReference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (true === ctype_digit($trimmedReference)) {
|
||||
return (int) $trimmedReference;
|
||||
}
|
||||
|
||||
if (null !== $normalizer) {
|
||||
$trimmedReference = $normalizer($trimmedReference);
|
||||
}
|
||||
|
||||
return $mapper($trimmedReference);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user