Files
myep/src/Service/ParticipantPricingCalculator.php
T

238 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
/**
* Calculates pricing for individual participants in a booking.
*
* Handles room allocation costs, service selections, and provides
* both inclusive and exclusive insurance calculations for eligibility.
*/
class ParticipantPricingCalculator
{
/** @var array<string, float> Request-scoped cache for participant prices */
private array $participantPriceCache = [];
public function __construct(
private readonly RoomPricingCalculator $roomPricingCalculator,
) {
}
/**
* Calculates the total price for an individual participant.
*
* This method calculates the complete price breakdown for a single participant,
* including their room allocation (full room price) and all selected services.
*
* @param BookingDto $bookingDto The booking data containing all participants
* @param int $participantIndex The index of the participant to calculate for
*
* @return float The total price for the specified participant
*/
public function calculateIndividualParticipantPrice(BookingDto $bookingDto, int $participantIndex): float
{
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return 0.0;
}
$totalPrice = 0.0;
// Add room price if participant is assigned to a room
if (null !== $participant->assignedRoomId) {
$room = $this->roomPricingCalculator->getRoomById($bookingDto, $participant->assignedRoomId);
if (null !== $room && null !== $room->price) {
// Each participant pays the full room price
$totalPrice += $room->price;
}
}
// Add service prices for this participant (with booking context for bulk insurance)
$totalPrice += $this->calculateParticipantServiceTotal($participant, true);
return $totalPrice;
}
/**
* Calculates individual prices for all participants in a booking.
*
* @param BookingDto $bookingDto The booking data containing all participants
*
* @return array<int, float> Array indexed by participant index containing individual prices
*/
public function calculateAllParticipantIndividualPrices(BookingDto $bookingDto): array
{
$participantPrices = [];
$participants = $bookingDto->getParticipants();
foreach ($participants as $index => $participant) {
$participantPrices[$index] = $this->calculateIndividualParticipantPrice($bookingDto, $index);
}
return $participantPrices;
}
/**
* Calculates the total price for an individual participant excluding insurance.
*
* This method is used for insurance eligibility filtering to avoid circular dependency
* where insurance selection affects travel price which affects insurance eligibility.
*
* Only includes services where versicherungsberechnung='J' in the XML. Services with
* versicherungsberechnung='N' (like CO2 compensation) are excluded from the calculation
* as per BPN API requirements for insurance tier determination.
*
* @param BookingDto $bookingDto The booking data containing all participants
* @param int $participantIndex The index of the participant to calculate for
*
* @return float The total price for the specified participant excluding insurance and non-calculated services
*/
public function calculateIndividualParticipantPriceExcludingInsurance(
BookingDto $bookingDto,
int $participantIndex,
): float {
$participant = $bookingDto->getParticipant($participantIndex);
if (null === $participant) {
return 0.0;
}
// Generate cache key based on participant state that affects pricing
$stateComponents = [
'room' => $participant->assignedRoomId ?? 'none',
'skiPass' => $participant->skiPass->id ?? 'none',
'rentalInsurance' => $participant->rentalInsurance->id ?? 'none',
'transportationOut' => $participant->transportationOutbound->id ?? 'none',
'transportationIn' => $participant->transportationInbound->id ?? 'none',
'pickup' => $participant->pickup->id ?? 'none',
'dropOff' => $participant->dropOff->id ?? 'none',
'parking' => $participant->parkingService->id ?? 'none',
'courses' => implode('_', array_map(fn ($s) => $s->id, $participant->courses)),
'additionalServices' => implode('_', array_map(fn ($s) => $s->id, $participant->additionalServices)),
'board' => implode('_', array_map(fn ($s) => $s->id, $participant->board)),
'rentals' => implode('_', array_map(fn ($s) => $s->id, $participant->rentals)),
];
$cacheKey = sprintf(
'participant_price_%d_%s',
$participantIndex,
md5(json_encode($stateComponents))
);
return $this->participantPriceCache[$cacheKey] ??= (function () use ($bookingDto, $participant) {
$totalPrice = 0.0;
// Add room price if participant is assigned to a room
if (null !== $participant->assignedRoomId) {
$room = $this->roomPricingCalculator->getRoomById($bookingDto, $participant->assignedRoomId);
if (null !== $room && null !== $room->price) {
// Each participant pays the full room price
$totalPrice += $room->price;
}
}
// Add service prices for this participant (excluding insurance)
// For insurance eligibility calculation, only include services marked with versicherungsberechnung='J'
$totalPrice += $this->calculateParticipantServiceTotal($participant, false, true);
return $totalPrice;
})();
}
/**
* Calculates the total service cost for a single participant.
*
* @param ParticipantDto $participant The participant to calculate services for
* @param bool $includeInsurance Whether to include insurance pricing (default: true)
* @param bool $onlyInsuranceCalculationServices Only include services with includeInInsuranceCalculation=true (default: false)
*
* @return float The total service cost for this participant
*/
public function calculateParticipantServiceTotal(
ParticipantDto $participant,
bool $includeInsurance = true,
bool $onlyInsuranceCalculationServices = false,
): float {
$serviceTotal = 0.0;
// Single service selections
if (null !== $participant->skiPass && null !== $participant->skiPass->price) {
if (false === $onlyInsuranceCalculationServices || true === $participant->skiPass->includeInInsuranceCalculation) {
$serviceTotal += $participant->skiPass->price;
}
}
if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) {
if (false === $onlyInsuranceCalculationServices || true === $participant->rentalInsurance->includeInInsuranceCalculation) {
$serviceTotal += $participant->rentalInsurance->price;
}
}
// Get effective insurance (considering bulk insurance for dependent participants)
// ApplicantInsuranceCascade materialises bulk and family assignments into the DTO as
// they happen, so the participant's own field is already the effective insurance.
$effectiveInsurance = $participant->insurance;
if ($includeInsurance && null !== $effectiveInsurance && null !== $effectiveInsurance->price) {
$serviceTotal += $effectiveInsurance->price;
}
// Transportation services
if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) {
if (false === $onlyInsuranceCalculationServices || true === $participant->transportationOutbound->includeInInsuranceCalculation) {
$serviceTotal += $participant->transportationOutbound->price;
}
}
if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) {
if (false === $onlyInsuranceCalculationServices || true === $participant->transportationInbound->includeInInsuranceCalculation) {
$serviceTotal += $participant->transportationInbound->price;
}
}
// Boarding surcharges are charged per leg, at the same rate in both directions
$hasOutboundBus = null !== $participant->transportationOutbound
&& DirectionMapper::isBus($participant->transportationOutbound->subType);
$hasInboundBus = null !== $participant->transportationInbound
&& DirectionMapper::isBus($participant->transportationInbound->subType);
if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) {
$serviceTotal += $participant->pickup->price;
}
if ($hasInboundBus && null !== $participant->dropOff && null !== $participant->dropOff->price) {
$serviceTotal += $participant->dropOff->price;
}
if (null !== $participant->parkingService && null !== $participant->parkingService->price) {
if (false === $onlyInsuranceCalculationServices || true === $participant->parkingService->includeInInsuranceCalculation) {
$serviceTotal += $participant->parkingService->price;
}
}
// Multiple service selections
$multipleServiceArrays = [
'courses' => $participant->courses,
'additionalServices' => $participant->additionalServices,
'board' => $participant->board,
'rentals' => $participant->rentals,
];
foreach ($multipleServiceArrays as $serviceArray) {
foreach ($serviceArray as $service) {
if (null !== $service->price) {
if (false === $onlyInsuranceCalculationServices || true === $service->includeInInsuranceCalculation) {
$serviceTotal += $service->price;
}
}
}
}
return $serviceTotal;
}
}