feat: groups price calculator admin crud, booking/offer flow and api

This commit is contained in:
Björn Fromme
2026-08-03 15:25:10 +02:00
parent eabed8295a
commit 9d2aa11fdb
258 changed files with 16231 additions and 582 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect;
use App\BpnConnect\Exception\BpnConnectException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
abstract class AbstractApiClient
{
public function __construct(
protected readonly HttpClientInterface $httpClient,
protected readonly LoggerInterface $logger,
protected readonly ?string $baseUrl = null,
protected readonly ?string $apiKey = null,
) {
}
/**
* @param array<string, mixed> $query
* @return array<string, mixed>
*/
protected function request(string $path, array $query = []): array
{
$this->assertConfigured();
try {
$response = $this->httpClient->request('GET', $this->baseUrl.$path, [
'headers' => [
'X-API-KEY' => $this->apiKey,
],
'query' => $query,
]);
return $response->toArray();
} catch (ExceptionInterface $e) {
$this->logger->error('BpnConnect request failed', [
'path' => $path,
'query' => $query,
'error' => $e->getMessage(),
]);
throw new BpnConnectException('BpnConnect request failed: '.$e->getMessage(), 0, $e);
}
}
private function assertConfigured(): void
{
if (null === $this->baseUrl || null === $this->apiKey) {
throw new BpnConnectException('BpnConnect client is not configured (missing BPN_CONNECT_BASE_URL or BPN_CONNECT_API_KEY).');
}
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect;
use App\BpnConnect\Model\ContingentCalendarEntry;
use App\BpnConnect\Model\ContingentCalendarMeta;
use App\BpnConnect\Model\ContingentCalendarResponse;
use App\BpnConnect\Model\ContingentMode;
use App\BpnConnect\Model\ContingentRangeEntry;
use App\BpnConnect\Model\ContingentRangeResponse;
use App\BpnConnect\Model\ContingentStatus;
class ContingentsClient extends AbstractApiClient
{
private const string CALENDAR_PATH = '/api/v1/contingents/calendar';
public function getContingentCalendar(
string $hotelCode,
string $dateFrom,
string $dateTo,
): ContingentCalendarResponse {
$payload = $this->request(self::CALENDAR_PATH, [
'hotelCode' => $hotelCode,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'mode' => ContingentMode::Days->value,
]);
$entries = array_map(
static fn (array $item) => new ContingentCalendarEntry(
date: $item['date'],
status: ContingentStatus::from($item['status']),
),
$payload['data'] ?? [],
);
return new ContingentCalendarResponse($this->buildMeta($payload), $entries);
}
public function getContingentRanges(
string $hotelCode,
string $dateFrom,
string $dateTo,
): ContingentRangeResponse {
$payload = $this->request(self::CALENDAR_PATH, [
'hotelCode' => $hotelCode,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
'mode' => ContingentMode::Ranges->value,
]);
$entries = array_map(
static fn (array $item) => new ContingentRangeEntry(
dateFrom: $item['date_from'],
dateTo: $item['date_to'],
status: ContingentStatus::from($item['status']),
),
$payload['data'] ?? [],
);
return new ContingentRangeResponse($this->buildMeta($payload), $entries);
}
/**
* @param array<string, mixed> $payload
*/
private function buildMeta(array $payload): ContingentCalendarMeta
{
$raw = $payload['meta'] ?? [];
return new ContingentCalendarMeta(
dateFrom: $raw['date_from'] ?? '',
dateTo: $raw['date_to'] ?? '',
mode: $raw['mode'] ?? '',
hotelCode: $raw['hotel_code'] ?? '',
hotelId: (int) ($raw['hotel_id'] ?? 0),
rowCount: (int) ($raw['row_count'] ?? 0),
);
}
}
@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Exception;
class BpnConnectException extends \RuntimeException
{
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
readonly class ContingentCalendarEntry
{
public function __construct(
public string $date,
public ContingentStatus $status,
) {
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
readonly class ContingentCalendarMeta
{
public function __construct(
public string $dateFrom,
public string $dateTo,
public string $mode,
public string $hotelCode,
public int $hotelId,
public int $rowCount,
) {
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
readonly class ContingentCalendarResponse
{
/**
* @param ContingentCalendarEntry[] $data
*/
public function __construct(
public ContingentCalendarMeta $meta,
public array $data,
) {
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
enum ContingentMode: string
{
case Days = 'days';
case Ranges = 'ranges';
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
readonly class ContingentRangeEntry
{
public function __construct(
public string $dateFrom,
public string $dateTo,
public ContingentStatus $status,
) {
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
readonly class ContingentRangeResponse
{
/**
* @param ContingentRangeEntry[] $data
*/
public function __construct(
public ContingentCalendarMeta $meta,
public array $data,
) {
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\BpnConnect\Model;
enum ContingentStatus: string
{
case Ok = 'OK';
case Blocked = 'BLOCKED';
case OnRequest = 'ON_REQUEST';
public function label(): string
{
return 'enum.contingent_status.'.strtolower($this->value);
}
}