Files
myep/src/Form/Service/ParticipantTransportationDiscountReplacementFieldHandler.php
T
2026-09-11 19:37:00 +02:00

217 lines
8.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\BusProNet\Constants;
use App\BusProNet\Model\Service;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingDto;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
use App\Service\ServiceAvailabilityCalculator;
/**
* Handles automatic replacement of discounted transportation based on booking rules.
*
* This virtual field handler enforces the business rule that discounted self-organized
* transportation (PKW with negative price) is only available when both outbound AND
* inbound travel use PKW. When inbound changes to BUS, the outbound PKW is automatically
* replaced with the regular (non-discounted) PKW variant.
*
* Conversely, when inbound changes back to PKW, this handler restores the discounted
* variant if availability permits. Baby participants (age <= 2) are excluded from
* discounted transportation restoration.
*/
class ParticipantTransportationDiscountReplacementFieldHandler extends AbstractParticipantFieldHandler
{
public function __construct(
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
) {
}
public function getFieldName(): string
{
return 'transportationDiscountReplacement'; // Virtual field name
}
public function getDependencies(): array
{
return ['transportationOutbound', 'transportationInbound'];
}
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
{
return true; // Always process in all modes
}
/**
* Processes transportation discount replacement logic.
*
* Detects incompatible transportation combinations and automatically replaces
* discounted PKW with regular variant when inbound bus is selected. Also restores
* discounted PKW when inbound changes back to PKW and discount is still available.
*
* Business Rules:
* - Inbound BUS + Outbound discounted PKW → Replace with regular PKW (notification: warning)
* - Inbound PKW + Outbound regular PKW → Restore discounted PKW if available (notification: success)
* - Only processes PKW variants, never touches BUS or other services
*/
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
{
// Step 1: Get participant and validate prerequisites
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return;
}
// Need both transportation fields to process
if (null === $participant->transportationOutbound) {
return;
}
// Step 2: Detect inbound transportation type
$inboundIsBus = null !== $participant->transportationInbound
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
// Step 3: Get all outbound PKW services
$allOutboundServices = $bookingDto->travel->getTransportationServicesByDirection(
DirectionMapper::OUTBOUND_TRAVEL
);
// Find discounted and regular PKW services
$discountedPkw = null;
$regularPkw = null;
foreach ($allOutboundServices as $service) {
if (false === DirectionMapper::isCar($service->subType)) {
continue; // Skip non-PKW services
}
if (null !== $service->price && $service->price < 0) {
$discountedPkw = $service;
} elseif (null === $service->price || $service->price >= 0) {
$regularPkw = $service;
}
}
// Edge case: no regular PKW found (impossible in production, but handle gracefully)
if (null === $regularPkw) {
return;
}
// Step 4a: Handle inbound BUS scenario (force regular PKW)
if (true === $inboundIsBus) {
// Check if outbound is currently DISCOUNTED PKW
$outboundIsDiscountedPkw = DirectionMapper::isCar($participant->transportationOutbound->subType)
&& null !== $participant->transportationOutbound->price
&& $participant->transportationOutbound->price < 0;
if ($outboundIsDiscountedPkw) {
// Store original for notification
$originalLabel = $participant->transportationOutbound->label;
// Replace with regular PKW
$participant->transportationOutbound = $regularPkw;
// Notify user about replacement
$participant->addNotification(
'warning',
sprintf(
'Hinfahrt automatisch angepasst: Rabattierte Anreise nicht möglich bei Busrückfahrt. Geändert von "%s" zu "%s".',
$originalLabel,
$regularPkw->label
)
);
$this->syncBookingTransportationService($bookingDto, $participantIndex, $discountedPkw, $regularPkw);
}
return; // Done processing for BUS inbound scenario
}
// Step 4b: Handle non-BUS inbound scenario (restore discount if available)
// Inbound is NOT BUS - try to restore discounted PKW if available
// Baby participants never get discounted transportation
$age = $participant->getAge($bookingDto->travel->dateFrom);
if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
return;
}
// Check if outbound is currently REGULAR PKW
$outboundIsRegularPkw = DirectionMapper::isCar($participant->transportationOutbound->subType)
&& (null === $participant->transportationOutbound->price
|| $participant->transportationOutbound->price >= 0);
if (false === $outboundIsRegularPkw || null === $discountedPkw) {
return; // Not eligible for discount restoration
}
// Check per-booking availability of discounted PKW
$discountedIsAvailable = false === $this->serviceAvailabilityCalculator->isServiceUnavailable(
$discountedPkw->id,
$bookingDto,
$participantIndex
);
if ($discountedIsAvailable) {
// Store original for notification
$originalLabel = $participant->transportationOutbound->label;
// Restore discounted PKW
$participant->transportationOutbound = $discountedPkw;
// Notify user about restoration
$participant->addNotification(
'success',
sprintf(
'Hinfahrt automatisch angepasst: Rabattierte Anreise wieder verfügbar. Geändert von "%s" zu "%s".',
$originalLabel,
$discountedPkw->label
)
);
$this->syncBookingTransportationService($bookingDto, $participantIndex, $regularPkw, $discountedPkw);
}
}
/**
* Keeps $bookingDto->booking->transportationServices consistent with a participant's
* updated outbound transportation selection. Only runs in edit mode.
*
* Clones the new service before inserting it to avoid mutating the shared travel-data
* object when resetServiceMappings() clears mappings at submit time.
*/
private function syncBookingTransportationService(
BookingDto $bookingDto,
int $participantIndex,
Service $oldService,
Service $newService,
): void {
if (null === $bookingDto->booking) {
return;
}
$services = &$bookingDto->booking->transportationServices;
if (isset($services[$oldService->id])) {
$services[$oldService->id]->mapping = array_values(
array_filter(
$services[$oldService->id]->mapping,
fn (int $idx): bool => $idx !== $participantIndex,
)
);
}
if (false === isset($services[$newService->id])) {
$services[$newService->id] = clone $newService;
$services[$newService->id]->mapping = [];
}
if (false === in_array($participantIndex, $services[$newService->id]->mapping, true)) {
$services[$newService->id]->mapping[] = $participantIndex;
}
}
}