feat: promotional and purchase vouchers
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Exception\BookingNotPossibleException;
|
||||
@@ -221,7 +222,7 @@ class BookingService
|
||||
// For inquiry bookings with 0 availability, get all rooms ignoring availability count
|
||||
if ($isInquiryBooking && empty($availableRooms)) {
|
||||
$availableRooms = array_filter($travelData->rooms, function (Room $room) {
|
||||
return \App\BusProNet\Constants::STATUS_AVAILABLE === $room->status;
|
||||
return Constants::STATUS_AVAILABLE === $room->status;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -443,7 +444,7 @@ class BookingService
|
||||
*/
|
||||
public function preselectMandatoryServices(BookingDto $bookingDto): void
|
||||
{
|
||||
$additionalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(\App\BusProNet\Constants::TOKEN_ADDITIONAL);
|
||||
$additionalServices = $bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_ADDITIONAL);
|
||||
$mandatoryServices = array_filter($additionalServices, fn ($service) => true === $service->mandatory);
|
||||
|
||||
// Pre-select mandatory services for each participant
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
/**
|
||||
* Consolidates all booking summary data for display in sidebars.
|
||||
*
|
||||
* Provides pricing breakdowns, room assignments, participant counts,
|
||||
* and CMS product information in a single service.
|
||||
*/
|
||||
class BookingSummaryDataService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BookingPriceCalculatorService $priceCalculator,
|
||||
private readonly CmsDataService $cmsDataService,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete summary data for booking sidebar.
|
||||
*
|
||||
* @return array{
|
||||
* selectedRooms: array,
|
||||
* participantCount: int,
|
||||
* totalPrice: string,
|
||||
* groupedSelectedRooms: array,
|
||||
* assignmentCounts: array,
|
||||
* pricingData: array,
|
||||
* cmsData: array|null
|
||||
* }
|
||||
*/
|
||||
public function getSummaryData(BookingDto $bookingDto): array
|
||||
{
|
||||
// Get selected rooms (for Step1 controller compatibility)
|
||||
$selectedRooms = $bookingDto->getSelectedRooms();
|
||||
|
||||
// Calculate individual prices for all participants
|
||||
$participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto);
|
||||
|
||||
// Calculate total price
|
||||
$totalPrice = array_sum($participantPrices);
|
||||
|
||||
// Get room assignment counts
|
||||
$roomCounts = [];
|
||||
foreach ($bookingDto->participants as $participant) {
|
||||
if (null !== $participant->assignedRoomId) {
|
||||
$roomCounts[$participant->assignedRoomId] = ($roomCounts[$participant->assignedRoomId] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Group selected rooms with counts
|
||||
$groupedSelectedRooms = [];
|
||||
foreach ($roomCounts as $roomId => $count) {
|
||||
$room = $bookingDto->travel->getRoomById($roomId);
|
||||
if (null !== $room) {
|
||||
$groupedSelectedRooms[] = [
|
||||
'room' => $room,
|
||||
'count' => $count,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get detailed pricing breakdown
|
||||
$pricingData = $this->priceCalculator->getPricingBreakdown($bookingDto);
|
||||
|
||||
// Fetch CMS data (images, etc.)
|
||||
$cmsData = $this->getCmsData($bookingDto);
|
||||
|
||||
return [
|
||||
'selectedRooms' => $selectedRooms,
|
||||
'participantCount' => count($bookingDto->participants),
|
||||
'totalPrice' => number_format($totalPrice, 2, ',', '.').' €',
|
||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||
'assignmentCounts' => $roomCounts,
|
||||
'pricingData' => $pricingData,
|
||||
'cmsData' => $cmsData,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches CMS data for the product and hotel in the booking.
|
||||
*
|
||||
* Data is cached for 1 hour as it rarely changes. Returns null
|
||||
* if the API call fails or if product/hotel codes are not available.
|
||||
*/
|
||||
private function getCmsData(BookingDto $bookingDto): ?array
|
||||
{
|
||||
$productCode = $bookingDto->travel->productCode;
|
||||
$hotelCode = $bookingDto->travel->hotel?->code;
|
||||
|
||||
if (null === $productCode) {
|
||||
$this->logger->debug('Cannot fetch CMS data: product code is not available', [
|
||||
'travel_id' => $bookingDto->travel->id,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = sprintf('cms_data.%s.%s', $productCode, $hotelCode ?? 'none');
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($productCode, $hotelCode) {
|
||||
$item->expiresAfter(3600); // 1 hour
|
||||
|
||||
$result = $this->cmsDataService->getProductDetails($productCode, $hotelCode);
|
||||
|
||||
// Return null if API call failed
|
||||
if (true === isset($result['success']) && false === $result['success']) {
|
||||
$this->logger->warning('CMS API call failed', [
|
||||
'product_code' => $productCode,
|
||||
'hotel_code' => $hotelCode,
|
||||
'message' => $result['message'] ?? 'Unknown error',
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Failed to fetch CMS data', [
|
||||
'product_code' => $productCode,
|
||||
'hotel_code' => $hotelCode,
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PromoVoucherValidationResult;
|
||||
use App\BusProNet\Model\PurchaseVoucherValidationResult;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
class VoucherValidationService
|
||||
{
|
||||
private const CACHE_TTL = 900; // 15 minutes
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly CacheInterface $cache,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function validatePurchaseVoucher(
|
||||
string $redemptionCode,
|
||||
): PurchaseVoucherValidationResult {
|
||||
$cacheKey = sprintf('voucher.purchase.%s', md5($redemptionCode));
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($redemptionCode) {
|
||||
$item->expiresAfter(self::CACHE_TTL);
|
||||
|
||||
try {
|
||||
$result = $this->apiClient->validatePurchaseVoucher($redemptionCode);
|
||||
|
||||
if ($result instanceof Notification) {
|
||||
return new PurchaseVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: $result->message,
|
||||
errorCode: $result->code
|
||||
);
|
||||
}
|
||||
|
||||
return new PurchaseVoucherValidationResult(
|
||||
isValid: true,
|
||||
voucher: $result
|
||||
);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Purchase voucher validation failed', [
|
||||
'redemptionCode' => $redemptionCode,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return new PurchaseVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: 'Die Gutschein-Validierung ist fehlgeschlagen.'
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Cache error during purchase voucher validation', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return new PurchaseVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: 'Ein technischer Fehler ist aufgetreten.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function validatePromoVoucher(
|
||||
string $promoCode,
|
||||
int $travelId,
|
||||
float $participantPrice,
|
||||
): PromoVoucherValidationResult {
|
||||
$cacheKey = sprintf(
|
||||
'voucher.promo.%s.%d.%s',
|
||||
md5($promoCode),
|
||||
$travelId,
|
||||
md5((string) $participantPrice)
|
||||
);
|
||||
|
||||
try {
|
||||
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($promoCode, $travelId, $participantPrice) {
|
||||
$item->expiresAfter(self::CACHE_TTL);
|
||||
|
||||
try {
|
||||
$result = $this->apiClient->validatePromoVoucher($promoCode, $travelId, $participantPrice);
|
||||
|
||||
if ($result instanceof Notification) {
|
||||
return new PromoVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: $result->message,
|
||||
errorCode: $result->code
|
||||
);
|
||||
}
|
||||
|
||||
return new PromoVoucherValidationResult(
|
||||
isValid: true,
|
||||
voucher: $result
|
||||
);
|
||||
} catch (ApiClientException $e) {
|
||||
$this->logger->error('Promo voucher validation failed', [
|
||||
'promoCode' => $promoCode,
|
||||
'travelId' => $travelId,
|
||||
'participantPrice' => $participantPrice,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return new PromoVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: 'Die Aktionscode-Validierung ist fehlgeschlagen.'
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Cache error during promo voucher validation', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return new PromoVoucherValidationResult(
|
||||
isValid: false,
|
||||
errorMessage: 'Ein technischer Fehler ist aufgetreten.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user