wip: insurance booking

This commit is contained in:
Björn Fromme
2026-03-16 11:59:10 +01:00
parent 4ba81b8f03
commit 29bdf794f3
11 changed files with 131 additions and 475 deletions
+20
View File
@@ -19,11 +19,31 @@ final class Constants
public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG']; public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG'];
public const TOKEN_PARKING = 'PAR'; public const TOKEN_PARKING = 'PAR';
// Insurance subtype labels for display
public const INSURANCE_LABELS = [
'RRV' => 'Reiserücktrittsversicherung',
'PAK' => 'Reiseschutz',
'OHN' => 'Selbstbehalt',
];
// Normalized service group keys for pricing display // Normalized service group keys for pricing display
public const GROUP_TRANSPORTATION = 'transportation'; public const GROUP_TRANSPORTATION = 'transportation';
public const GROUP_RENTALS = 'rentals'; public const GROUP_RENTALS = 'rentals';
public const GROUP_INSURANCE = 'insurance'; public const GROUP_INSURANCE = 'insurance';
// Service type display labels (German) - used in pricing calculator
public const SERVICE_LABELS = [
self::TOKEN_COURSES => 'Kurse',
self::TOKEN_SKI_PASS => 'Skipässe',
self::TOKEN_ADDITIONAL => 'Zusatzleistungen',
self::TOKEN_BOARD => 'Verpflegung',
self::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung',
self::TOKEN_PARKING => 'Parkplatz',
self::GROUP_TRANSPORTATION => 'Beförderung',
self::GROUP_RENTALS => 'Leihmaterial',
self::GROUP_INSURANCE => 'Reiseversicherungen',
];
public const STATUS_AVAILABLE = 'Frei'; public const STATUS_AVAILABLE = 'Frei';
public const STATUS_BLOCKED = 'Buchungsstop'; public const STATUS_BLOCKED = 'Buchungsstop';
public const STATUS_ON_REQUEST = 'Anfrage'; public const STATUS_ON_REQUEST = 'Anfrage';
@@ -44,33 +44,6 @@ class InsuranceLoader extends AbstractLoader
return $insurances[$id] ?? null; return $insurances[$id] ?? null;
} }
/**
* Loads all insurances with resolved package relationships.
*
* @param string|null $filename The XML filename to load from
* @return array<string|int, Insurance> Array of Insurance objects with resolved relationships
*/
public function loadAllWithRelationships(?string $filename = 'versicherungen.xml'): array
{
$insurances = $this->loadAll($filename);
// Resolve package relationships
foreach ($insurances as $insurance) {
if ($insurance->package && !empty($insurance->containedInsuranceIds)) {
foreach ($insurance->containedInsuranceIds as $containedId) {
if (isset($insurances[$containedId])) {
$insurance->containedInsurances[] = $insurances[$containedId];
} else {
// Log missing insurance reference for debugging
error_log("Insurance package {$insurance->id} references missing insurance {$containedId}");
}
}
}
}
return $insurances;
}
private function loadXml(?string $filename = 'versicherungen.xml'): Crawler private function loadXml(?string $filename = 'versicherungen.xml'): Crawler
{ {
$xml = $this->xmlExport->read($filename); $xml = $this->xmlExport->read($filename);
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Form\Service; namespace App\Form\Service;
use App\BusProNet\Model\Insurance;
use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Service; use App\BusProNet\Model\Service;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
@@ -345,6 +346,11 @@ class ParticipantFieldHandlerRegistry
return $value->id; return $value->id;
} }
// Handle Insurance objects -> convert to ID
if ($value instanceof Insurance) {
return $value->id;
}
// Handle DateTimeInterface -> convert to string format // Handle DateTimeInterface -> convert to string format
if ($value instanceof \DateTimeInterface) { if ($value instanceof \DateTimeInterface) {
return $value->format('Y-m-d'); return $value->format('Y-m-d');
@@ -13,7 +13,6 @@ use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingDtoInterface; use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
use App\Form\Service\ServiceAgeEvaluator;
use App\Service\InsuranceMatchingService; use App\Service\InsuranceMatchingService;
use App\Service\ServiceAvailabilityCalculator; use App\Service\ServiceAvailabilityCalculator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -591,7 +590,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
private function getParkingCheckboxLabel(array $parkingServices): string private function getParkingCheckboxLabel(array $parkingServices): string
{ {
if (empty($parkingServices)) { if (empty($parkingServices)) {
return 'Parkplatz'; return Constants::SERVICE_LABELS[Constants::TOKEN_PARKING];
} }
$parkingService = reset($parkingServices); // Get the first (and only) parking service $parkingService = reset($parkingServices); // Get the first (and only) parking service
@@ -681,7 +680,7 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
private function getRentalInsuranceCheckboxLabel(array $rentalInsuranceServices): string private function getRentalInsuranceCheckboxLabel(array $rentalInsuranceServices): string
{ {
if (empty($rentalInsuranceServices)) { if (empty($rentalInsuranceServices)) {
return 'Leihmaterial-Versicherung'; return Constants::SERVICE_LABELS[Constants::TOKEN_RENTAL_INSURANCE];
} }
$rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service $rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service
@@ -751,15 +750,9 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
$label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.')); $label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.'));
} }
// Add type information if available // Add type information if available using centralized mapping
if (null !== $insurance->subType) { if (null !== $insurance->subType && isset(Constants::INSURANCE_LABELS[$insurance->subType])) {
$typeLabel = match ($insurance->subType) { $label .= sprintf(' (%s)', Constants::INSURANCE_LABELS[$insurance->subType]);
'RRV' => 'Reiserücktrittsversicherung',
'PAK' => 'Reiseschutz',
'OHN' => 'Selbstbehalt',
default => $insurance->subType,
};
$label .= sprintf(' (%s)', $typeLabel);
} }
return $label; return $label;
@@ -35,7 +35,7 @@ use App\Service\InsuranceMatchingService;
class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
{ {
public function __construct( public function __construct(
private readonly InsuranceMatchingService $insuranceMatchingService private readonly InsuranceMatchingService $insuranceMatchingService,
) { ) {
} }
@@ -52,13 +52,27 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
/** /**
* Returns the field dependencies for proper processing order. * Returns the field dependencies for proper processing order.
* *
* This handler depends on dateOfBirth for age evaluation and insurance eligibility. * This handler must run AFTER all fields that affect travel price, since insurance
* reassignment logic depends on accurate price calculations. It also depends on
* dateOfBirth for age evaluation and insurance eligibility.
* *
* @return string[] Array containing 'dateOfBirth' dependency * @return string[] Array of field dependencies
*/ */
public function getDependencies(): array public function getDependencies(): array
{ {
return ['dateOfBirth']; return [
'dateOfBirth', // Required for age-based eligibility
'skiPass', // Affects travel price
'rentals', // Affects travel price
'courses', // Affects travel price
'additionalServices', // Affects travel price
'board', // Affects travel price
'transportationOutbound', // Affects travel price
'transportationInbound', // Affects travel price
'pickupOutbound', // Affects travel price
'pickupInbound', // Affects travel price
'parking', // Affects travel price
];
} }
/** /**
@@ -82,8 +96,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
* Processes the insurance field for a specific participant. * Processes the insurance field for a specific participant.
* *
* This method handles both explicit insurance selection from form data and automatic * This method handles both explicit insurance selection from form data and automatic
* reassignment when the participant's travel price changes. It maintains the same * reassignment when the participant's travel price changes. It distinguishes between:
* insurance type but adjusts to the appropriate price tier when needed. * 1. New selection: User explicitly changed their insurance choice
* 2. Resubmission: Form resubmitted with existing insurance (e.g., after adding rentals)
*
* For resubmissions, it checks if the current insurance is still eligible with updated
* participant data and automatically reassigns to the correct price tier if needed.
* *
* @param array<string, mixed> $submittedData The submitted participant form data * @param array<string, mixed> $submittedData The submitted participant form data
* @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit) * @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit)
@@ -100,59 +118,63 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
$currentInsurance = $participant->insurance; $currentInsurance = $participant->insurance;
$availableInsurances = $bookingDto->travel->insurances ?? []; $availableInsurances = $bookingDto->travel->insurances ?? [];
// Handle explicit insurance selection from form // Determine if this is a new user selection or just form resubmission
if (null !== $selectedInsuranceId) { $isNewSelection = null !== $selectedInsuranceId
&& (null === $currentInsurance || !$this->isSameInsurance($selectedInsuranceId, $currentInsurance));
// Handle explicit new insurance selection from user
if ($isNewSelection) {
$selectedInsurance = $this->findInsuranceById($availableInsurances, $selectedInsuranceId); $selectedInsurance = $this->findInsuranceById($availableInsurances, $selectedInsuranceId);
// Check if the selected insurance is still eligible for this participant
if (null !== $selectedInsurance) { if (null !== $selectedInsurance) {
$eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto);
$isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances); $isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances);
if ($isSelectedInsuranceEligible) { if ($isSelectedInsuranceEligible) {
// Insurance is still eligible - use it directly // New selection is eligible - use it
$participant->insurance = $selectedInsurance; $participant->insurance = $selectedInsurance;
return;
} else { } else {
// Insurance is no longer eligible - try to reassign to same type with new price tier // New selection is not eligible - try to find alternative in same type
$reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange(
$availableInsurances, $availableInsurances,
$selectedInsurance, $selectedInsurance,
$participant, $participant,
$bookingDto $bookingDto
); );
$participant->insurance = $reassignedInsurance; $participant->insurance = $reassignedInsurance;
return;
} }
} else { } else {
// Insurance not found - clear selection // Insurance not found - clear selection
$participant->insurance = null; $participant->insurance = null;
return;
}
} }
// Handle automatic reassignment if participant had an insurance but it's no longer eligible return;
if (null !== $currentInsurance) { }
// Check if current insurance is still eligible
// Handle form resubmission with existing insurance (automatic reassignment check)
if (null !== $currentInsurance && null !== $selectedInsuranceId) {
// Check if current insurance is still eligible with updated participant data
$eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto);
$isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances); $isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances);
if (!$isCurrentInsuranceStillEligible) { if (!$isCurrentInsuranceStillEligible) {
// Try to reassign to same insurance type with appropriate price tier // Current insurance no longer eligible - try to reassign to same type with new price tier
$reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange(
$availableInsurances, $availableInsurances,
$currentInsurance, $currentInsurance,
$participant, $participant,
$bookingDto $bookingDto
); );
$participant->insurance = $reassignedInsurance; // null if no suitable match found $participant->insurance = $reassignedInsurance; // null if no suitable match found
}
return; return;
} }
}
// No insurance selected or reassignment needed - keep current state (may be null) // Handle explicit deselection (user removed insurance)
if (null === $selectedInsuranceId) {
$participant->insurance = null;
}
} }
/** /**
@@ -221,4 +243,19 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
return false; return false;
} }
/**
* Checks if the submitted insurance ID matches the current insurance.
*
* This is used to distinguish between a new user selection and a form resubmission
* with the existing insurance selection (e.g., when user adds rentals that change travel price).
*
* @param string|int $selectedInsuranceId The insurance ID from form submission
* @param Insurance $currentInsurance The currently assigned insurance from DTO
*
* @return bool True if they represent the same insurance
*/
private function isSameInsurance(string|int $selectedInsuranceId, Insurance $currentInsurance): bool
{
return (string) $currentInsurance->id === (string) $selectedInsuranceId;
}
} }
+3 -14
View File
@@ -359,7 +359,7 @@ class BookingPriceCalculatorService
if ($parkingTotal > 0) { if ($parkingTotal > 0) {
$transportationItems['transportation_parking'] = [ $transportationItems['transportation_parking'] = [
'serviceId' => 'transportation_parking', 'serviceId' => 'transportation_parking',
'label' => 'Parkplatz', 'label' => Constants::SERVICE_LABELS[Constants::TOKEN_PARKING],
'unitPrice' => null, 'unitPrice' => null,
'participantCount' => $parkingParticipants, 'participantCount' => $parkingParticipants,
'totalPrice' => $parkingTotal, 'totalPrice' => $parkingTotal,
@@ -444,23 +444,12 @@ class BookingPriceCalculatorService
*/ */
private function getGroupNameForSubtype(string $subType): string private function getGroupNameForSubtype(string $subType): string
{ {
$groupMapping = [
Constants::TOKEN_COURSES => 'Kurse',
Constants::TOKEN_SKI_PASS => 'Skipässe',
Constants::TOKEN_ADDITIONAL => 'Zusatzleistungen',
Constants::TOKEN_BOARD => 'Verpflegung',
Constants::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung',
Constants::GROUP_TRANSPORTATION => 'Beförderung',
Constants::GROUP_RENTALS => 'Leihmaterial', // Normalized rental subtype
Constants::GROUP_INSURANCE => 'Reiseversicherungen', // Normalized insurance subtype
];
// Handle rentals array (keep for backward compatibility with non-normalized subtypes) // Handle rentals array (keep for backward compatibility with non-normalized subtypes)
if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) {
return 'Leihmaterial'; return Constants::SERVICE_LABELS[Constants::GROUP_RENTALS];
} }
return $groupMapping[$subType] ?? 'Sonstige Leistungen'; return Constants::SERVICE_LABELS[$subType] ?? 'Sonstige Leistungen';
} }
/** /**
+11 -3
View File
@@ -8,8 +8,6 @@ use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Model\InsuranceEligibilityCriteria; use App\Model\InsuranceEligibilityCriteria;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceTypeResolver;
use Carbon\Carbon; use Carbon\Carbon;
/** /**
@@ -23,9 +21,9 @@ class InsuranceMatchingService
{ {
public function __construct( public function __construct(
private readonly BookingPriceCalculatorService $priceCalculatorService, private readonly BookingPriceCalculatorService $priceCalculatorService,
private readonly InsuranceTypeResolver $insuranceTypeResolver
) { ) {
} }
/** /**
* Filters insurances based on participant and booking criteria. * Filters insurances based on participant and booking criteria.
* *
@@ -78,6 +76,10 @@ class InsuranceMatchingService
/** /**
* Batch-assigns insurances of the same type to all participants based on individual pricing. * Batch-assigns insurances of the same type to all participants based on individual pricing.
* *
* FUTURE FEATURE: This method will be used when implementing the "applicant assigns
* insurance to all participants" feature. The applicant's selection will be propagated
* to all participants with automatic price tier adjustment based on individual prices.
*
* This method takes the applicant's insurance selection and assigns the same insurance type * This method takes the applicant's insurance selection and assigns the same insurance type
* (subType + familyInsurance) to all participants, but selects the appropriate price tier * (subType + familyInsurance) to all participants, but selects the appropriate price tier
* based on each participant's individual travel price. * based on each participant's individual travel price.
@@ -87,6 +89,8 @@ class InsuranceMatchingService
* @param BookingCreateDto $booking The booking with all participants * @param BookingCreateDto $booking The booking with all participants
* *
* @return array<int, Insurance|null> Array indexed by participant index with assigned insurances * @return array<int, Insurance|null> Array indexed by participant index with assigned insurances
*
* @internal Reserved for future feature implementation
*/ */
public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingCreateDto $booking): array public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingCreateDto $booking): array
{ {
@@ -106,9 +110,13 @@ class InsuranceMatchingService
/** /**
* Creates eligibility criteria from participant and booking data. * Creates eligibility criteria from participant and booking data.
*
* This method performs early validation before creating the criteria object
* to avoid unnecessary object instantiation when criteria cannot be satisfied.
*/ */
private function createEligibilityCriteria(ParticipantDto $participant, BookingCreateDto $booking): ?InsuranceEligibilityCriteria private function createEligibilityCriteria(ParticipantDto $participant, BookingCreateDto $booking): ?InsuranceEligibilityCriteria
{ {
// Early return if travel dates are missing - cannot evaluate any criteria
$travelStartDate = $booking->travel->dateFrom; $travelStartDate = $booking->travel->dateFrom;
$travelEndDate = $booking->travel->dateTo; $travelEndDate = $booking->travel->dateTo;
-135
View File
@@ -1,135 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Model\Insurance;
/**
* Resolves insurance types for both individual insurances and packages.
*
* This service provides consistent type resolution by mapping insurance subtypes
* to standardized type constants. For packages, it analyzes contained insurances
* to determine the dominant (non-deductible) type and applies family variants
* based on the familyInsurance flag.
*/
class InsuranceTypeResolver
{
public const TRAVEL_CANCELLATION = 'TRAVEL_CANCELLATION';
public const TRAVEL_CANCELLATION_FAMILY = 'TRAVEL_CANCELLATION_FAMILY';
public const TRAVEL_PROTECTION = 'TRAVEL_PROTECTION';
public const TRAVEL_PROTECTION_FAMILY = 'TRAVEL_PROTECTION_FAMILY';
public const DEDUCTIBLE = 'DEDUCTIBLE';
/**
* Resolves the standardized type for an insurance or package.
*
* @param Insurance $insurance The insurance to resolve the type for
*
* @return string|null The resolved type constant or null if unresolvable
*/
public function resolveType(Insurance $insurance): ?string
{
if (!$insurance->package) {
return $this->mapSubtypeToType($insurance->subType, $insurance->familyInsurance);
}
return $this->resolvePackageType($insurance);
}
/**
* Resolves the type for an insurance package by analyzing contained insurances.
*
* @param Insurance $package The package to analyze
*
* @return string|null The resolved type or null if no primary type found
*/
private function resolvePackageType(Insurance $package): ?string
{
// Get primary (non-deductible) insurance type from contained insurances
$primaryType = null;
foreach ($package->containedInsurances as $contained) {
$baseType = $this->mapSubtypeToBaseType($contained->subType);
if ($baseType && self::DEDUCTIBLE !== $baseType) {
$primaryType = $baseType;
break;
}
}
// Apply family suffix using package's familyInsurance flag
return $primaryType ? $primaryType.($package->familyInsurance ? '_FAMILY' : '') : null;
}
/**
* Maps a subtype to a full type including family variant.
*
* @param string|null $subtype The subtype to map
* @param bool $isFamily Whether this is a family insurance
*
* @return string|null The mapped type or null if unmappable
*/
private function mapSubtypeToType(?string $subtype, bool $isFamily): ?string
{
$baseType = $this->mapSubtypeToBaseType($subtype);
return $baseType ? $baseType.($isFamily ? '_FAMILY' : '') : null;
}
/**
* Maps a subtype to its base type constant.
*
* @param string|null $subtype The subtype to map
*
* @return string|null The base type constant or null if unmappable
*/
private function mapSubtypeToBaseType(?string $subtype): ?string
{
return match ($subtype) {
'RRV' => self::TRAVEL_CANCELLATION,
'PAK' => self::TRAVEL_PROTECTION,
'OHN' => self::DEDUCTIBLE,
default => null,
};
}
/**
* Gets all available insurance type constants.
*
* @return array<string> Array of all type constants
*/
public function getAllTypes(): array
{
return [
self::TRAVEL_CANCELLATION,
self::TRAVEL_CANCELLATION_FAMILY,
self::TRAVEL_PROTECTION,
self::TRAVEL_PROTECTION_FAMILY,
self::DEDUCTIBLE,
];
}
/**
* Checks if a type is a family variant.
*
* @param string $type The type to check
*
* @return bool True if the type is a family variant
*/
public function isFamilyType(string $type): bool
{
return str_ends_with($type, '_FAMILY');
}
/**
* Gets the base type from a family type.
*
* @param string $type The type to get the base for
*
* @return string The base type (without _FAMILY suffix)
*/
public function getBaseType(string $type): string
{
return str_replace('_FAMILY', '', $type);
}
}
+13 -1
View File
@@ -95,7 +95,7 @@
} }
}) }} }) }}
{% else %} {% else %}
<div></div>{# Empty div to maintain grid layout when skipass not available #} <div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.courses is defined %} {% if participant.courses is defined %}
{{ form_row(participant.courses, { {{ form_row(participant.courses, {
@@ -105,6 +105,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.additionalServices is defined %} {% if participant.additionalServices is defined %}
{{ form_row(participant.additionalServices, { {{ form_row(participant.additionalServices, {
@@ -114,6 +116,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.rentals is defined %} {% if participant.rentals is defined %}
{{ form_row(participant.rentals, { {{ form_row(participant.rentals, {
@@ -123,6 +127,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.rentalInsurance is defined %} {% if participant.rentalInsurance is defined %}
{{ form_row(participant.rentalInsurance, { {{ form_row(participant.rentalInsurance, {
@@ -132,6 +138,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.board is defined %} {% if participant.board is defined %}
{{ form_row(participant.board, { {{ form_row(participant.board, {
@@ -141,6 +149,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
{% if participant.insurance is defined %} {% if participant.insurance is defined %}
{{ form_row(participant.insurance, { {{ form_row(participant.insurance, {
@@ -150,6 +160,8 @@
'hx-swap': 'none' 'hx-swap': 'none'
} }
}) }} }) }}
{% else %}
<div></div>{# Empty div to maintain grid layout when not available #}
{% endif %} {% endif %}
</div> </div>
+11 -1
View File
@@ -7,6 +7,7 @@ namespace App\Tests\Service;
use App\BusProNet\Model\Insurance; use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingCreateDto; use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto; use App\Form\Model\ParticipantDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceMatchingService; use App\Service\InsuranceMatchingService;
use Carbon\Carbon; use Carbon\Carbon;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -14,10 +15,17 @@ use PHPUnit\Framework\TestCase;
class InsuranceMatchingServiceTest extends TestCase class InsuranceMatchingServiceTest extends TestCase
{ {
private InsuranceMatchingService $service; private InsuranceMatchingService $service;
private BookingPriceCalculatorService $priceCalculator;
protected function setUp(): void protected function setUp(): void
{ {
$this->service = new InsuranceMatchingService(); // Mock the price calculator service
$this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
$this->priceCalculator
->method('calculateIndividualParticipantPriceExcludingInsurance')
->willReturn(500.0); // Default test price
$this->service = new InsuranceMatchingService($this->priceCalculator);
// Set a fixed test date for consistent test results // Set a fixed test date for consistent test results
Carbon::setTestNow('2024-06-01 12:00:00'); Carbon::setTestNow('2024-06-01 12:00:00');
@@ -164,6 +172,7 @@ class InsuranceMatchingServiceTest extends TestCase
public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void
{ {
$participant = new ParticipantDto(); $participant = new ParticipantDto();
$participant->index = 0; // Set participant index for price calculations
$participant->dateOfBirth = null; $participant->dateOfBirth = null;
$booking = $this->createBooking('2024-08-01', '2024-08-08'); $booking = $this->createBooking('2024-08-01', '2024-08-08');
@@ -239,6 +248,7 @@ class InsuranceMatchingServiceTest extends TestCase
private function createParticipant(string $dateOfBirth): ParticipantDto private function createParticipant(string $dateOfBirth): ParticipantDto
{ {
$participant = new ParticipantDto(); $participant = new ParticipantDto();
$participant->index = 0; // Set participant index for price calculations
$participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth); $participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth);
return $participant; return $participant;
-257
View File
@@ -1,257 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Insurance;
use App\Service\InsuranceTypeResolver;
use PHPUnit\Framework\TestCase;
class InsuranceTypeResolverTest extends TestCase
{
private InsuranceTypeResolver $resolver;
protected function setUp(): void
{
$this->resolver = new InsuranceTypeResolver();
}
public function testResolveTypeForIndividualTravelCancellationInsurance(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'RRV';
$insurance->familyInsurance = false;
$result = $this->resolver->resolveType($insurance);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result);
}
public function testResolveTypeForIndividualFamilyTravelCancellationInsurance(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'RRV';
$insurance->familyInsurance = true;
$result = $this->resolver->resolveType($insurance);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY, $result);
}
public function testResolveTypeForIndividualTravelProtectionInsurance(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'PAK';
$insurance->familyInsurance = false;
$result = $this->resolver->resolveType($insurance);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
}
public function testResolveTypeForIndividualFamilyTravelProtectionInsurance(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'PAK';
$insurance->familyInsurance = true;
$result = $this->resolver->resolveType($insurance);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result);
}
public function testResolveTypeForIndividualDeductibleInsurance(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'OHN';
$insurance->familyInsurance = false;
$result = $this->resolver->resolveType($insurance);
$this->assertEquals(InsuranceTypeResolver::DEDUCTIBLE, $result);
}
public function testResolveTypeForIndividualUnknownSubtype(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = 'UNKNOWN';
$insurance->familyInsurance = false;
$result = $this->resolver->resolveType($insurance);
$this->assertNull($result);
}
public function testResolveTypeForIndividualNullSubtype(): void
{
$insurance = new Insurance();
$insurance->package = false;
$insurance->subType = null;
$insurance->familyInsurance = false;
$result = $this->resolver->resolveType($insurance);
$this->assertNull($result);
}
public function testResolveTypeForPackageWithTravelProtectionContent(): void
{
// Create contained insurances
$protectionInsurance = new Insurance();
$protectionInsurance->subType = 'PAK';
$deductibleInsurance = new Insurance();
$deductibleInsurance->subType = 'OHN';
// Create package
$package = new Insurance();
$package->package = true;
$package->familyInsurance = false;
$package->containedInsurances = [$protectionInsurance, $deductibleInsurance];
$result = $this->resolver->resolveType($package);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
}
public function testResolveTypeForFamilyPackageWithTravelProtectionContent(): void
{
// Create contained insurances
$protectionInsurance = new Insurance();
$protectionInsurance->subType = 'PAK';
$deductibleInsurance = new Insurance();
$deductibleInsurance->subType = 'OHN';
// Create family package
$package = new Insurance();
$package->package = true;
$package->familyInsurance = true;
$package->containedInsurances = [$protectionInsurance, $deductibleInsurance];
$result = $this->resolver->resolveType($package);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY, $result);
}
public function testResolveTypeForPackageWithTravelCancellationContent(): void
{
// Create contained insurances
$cancellationInsurance = new Insurance();
$cancellationInsurance->subType = 'RRV';
$deductibleInsurance = new Insurance();
$deductibleInsurance->subType = 'OHN';
// Create package
$package = new Insurance();
$package->package = true;
$package->familyInsurance = false;
$package->containedInsurances = [$cancellationInsurance, $deductibleInsurance];
$result = $this->resolver->resolveType($package);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_CANCELLATION, $result);
}
public function testResolveTypeForPackageWithOnlyDeductibleContent(): void
{
// Create contained insurance (only deductible)
$deductibleInsurance = new Insurance();
$deductibleInsurance->subType = 'OHN';
// Create package
$package = new Insurance();
$package->package = true;
$package->familyInsurance = false;
$package->containedInsurances = [$deductibleInsurance];
$result = $this->resolver->resolveType($package);
$this->assertNull($result);
}
public function testResolveTypeForPackageWithNoContainedInsurances(): void
{
$package = new Insurance();
$package->package = true;
$package->familyInsurance = false;
$package->containedInsurances = [];
$result = $this->resolver->resolveType($package);
$this->assertNull($result);
}
public function testResolveTypeForPackageWithMultiplePrimaryTypes(): void
{
// Create contained insurances with multiple primary types
$protectionInsurance = new Insurance();
$protectionInsurance->subType = 'PAK';
$cancellationInsurance = new Insurance();
$cancellationInsurance->subType = 'RRV';
// Create package - should return first non-deductible type found
$package = new Insurance();
$package->package = true;
$package->familyInsurance = false;
$package->containedInsurances = [$protectionInsurance, $cancellationInsurance];
$result = $this->resolver->resolveType($package);
$this->assertEquals(InsuranceTypeResolver::TRAVEL_PROTECTION, $result);
}
public function testGetAllTypes(): void
{
$types = $this->resolver->getAllTypes();
$expectedTypes = [
InsuranceTypeResolver::TRAVEL_CANCELLATION,
InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY,
InsuranceTypeResolver::TRAVEL_PROTECTION,
InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY,
InsuranceTypeResolver::DEDUCTIBLE,
];
$this->assertEquals($expectedTypes, $types);
$this->assertCount(5, $types);
}
public function testIsFamilyType(): void
{
$this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY));
$this->assertTrue($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY));
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_CANCELLATION));
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::TRAVEL_PROTECTION));
$this->assertFalse($this->resolver->isFamilyType(InsuranceTypeResolver::DEDUCTIBLE));
}
public function testGetBaseType(): void
{
$this->assertEquals(
InsuranceTypeResolver::TRAVEL_CANCELLATION,
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION_FAMILY)
);
$this->assertEquals(
InsuranceTypeResolver::TRAVEL_PROTECTION,
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_PROTECTION_FAMILY)
);
$this->assertEquals(
InsuranceTypeResolver::TRAVEL_CANCELLATION,
$this->resolver->getBaseType(InsuranceTypeResolver::TRAVEL_CANCELLATION)
);
$this->assertEquals(
InsuranceTypeResolver::DEDUCTIBLE,
$this->resolver->getBaseType(InsuranceTypeResolver::DEDUCTIBLE)
);
}
}