feat: correct handling of vouchers
This commit is contained in:
@@ -204,7 +204,7 @@ class ApiClient
|
||||
'user' => $this->config['bpn_username'],
|
||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
|
||||
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
|
||||
'buchungsart' => 'Buchung',
|
||||
'buchungsart' => Constants::BOOKING_TYPE_BOOKING,
|
||||
...$payload,
|
||||
];
|
||||
|
||||
@@ -228,7 +228,7 @@ class ApiClient
|
||||
{
|
||||
// Forcibly override booking status to generate inquiry payload
|
||||
$bookingDto->bookingStatus = 'A';
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Anfrage');
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_INQUIRY);
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
@@ -255,7 +255,7 @@ class ApiClient
|
||||
*/
|
||||
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
|
||||
{
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Buchung');
|
||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_BOOKING);
|
||||
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
|
||||
@@ -48,6 +48,10 @@ final class Constants
|
||||
public const STATUS_BLOCKED = 'Buchungsstop';
|
||||
public const STATUS_ON_REQUEST = 'Anfrage';
|
||||
|
||||
// Booking types for API requests
|
||||
public const BOOKING_TYPE_INQUIRY = 'Anfrage';
|
||||
public const BOOKING_TYPE_BOOKING = 'Buchung';
|
||||
|
||||
// Payment methods
|
||||
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
||||
public const PAYMENT_METHOD_DEBIT = 'debit';
|
||||
|
||||
@@ -688,11 +688,16 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
// Add promo voucher or goodwill voucher as aktionscode
|
||||
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
||||
$aktionscode = $this->getPromoVoucherCodeForParticipant($dto);
|
||||
if (null !== $aktionscode) {
|
||||
$participantPayload['aktionscode'] = $aktionscode;
|
||||
// Add promotional voucher as promotionalCode
|
||||
$promotionalCode = $this->getPromoVoucherCodeForParticipant($dto);
|
||||
if (null !== $promotionalCode) {
|
||||
$participantPayload['aktionscode'] = $promotionalCode;
|
||||
}
|
||||
|
||||
// Add goodwill voucher as participant-level einloesecode
|
||||
$goodwillCode = $this->getGoodwillVoucherCodeForParticipant($dto);
|
||||
if (null !== $goodwillCode) {
|
||||
$participantPayload['einloesecode'] = $goodwillCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,6 +840,9 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this is an inquiry booking for voucher handling
|
||||
$isInquiryBooking = 'A' === $bookingDto->bookingStatus;
|
||||
|
||||
// Add participants
|
||||
$payload['teilnehmerliste']['teilnehmer'] = [];
|
||||
foreach ($bookingDto->participants as $index => $participant) {
|
||||
@@ -885,11 +893,19 @@ class BookingDataProcessor
|
||||
}
|
||||
}
|
||||
|
||||
// Add promo voucher or goodwill voucher as aktionscode
|
||||
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
||||
$aktionscode = $this->getPromoVoucherCodeForParticipant($participant);
|
||||
if (null !== $aktionscode) {
|
||||
$participantData['aktionscode'] = $aktionscode;
|
||||
// Add promotional voucher as aktionscode (allowed for inquiry bookings)
|
||||
$promotionalCode = $this->getPromoVoucherCodeForParticipant($participant);
|
||||
if (null !== $promotionalCode) {
|
||||
$participantData['aktionscode'] = $promotionalCode;
|
||||
}
|
||||
|
||||
// Add goodwill voucher as participant-level einloesecode
|
||||
// Goodwill vouchers are excluded from inquiry bookings and only allowed in final booking requests
|
||||
if (false === $isInquiryBooking && Constants::BOOKING_TYPE_BOOKING === $bookingType) {
|
||||
$goodwillCode = $this->getGoodwillVoucherCodeForParticipant($participant);
|
||||
if (null !== $goodwillCode) {
|
||||
$participantData['einloesecode'] = $goodwillCode;
|
||||
}
|
||||
}
|
||||
|
||||
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
|
||||
@@ -909,7 +925,9 @@ class BookingDataProcessor
|
||||
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
||||
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
||||
|
||||
// Add purchase vouchers if any exist
|
||||
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
|
||||
// Purchase vouchers are allowed for both inquiry and final bookings
|
||||
// Goodwill vouchers are handled separately and excluded from inquiry bookings
|
||||
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
|
||||
if (false === empty($purchaseVouchers)) {
|
||||
$payload['gutscheine']['gutschein'] = [];
|
||||
@@ -1196,22 +1214,16 @@ class BookingDataProcessor
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aktionscode for a participant.
|
||||
* Gets the aktionscode (promotional voucher code) for a participant.
|
||||
*
|
||||
* Checks if the participant has a goodwill (Kulanz) purchase voucher first.
|
||||
* If yes, returns that code (goodwill vouchers override promo vouchers).
|
||||
* Otherwise, returns the promo voucher code if present.
|
||||
* Returns the promotional voucher code if present.
|
||||
* Goodwill vouchers are handled separately via getGoodwillVoucherCodeForParticipant().
|
||||
*
|
||||
* @return string|null The aktionscode to use, or null if none
|
||||
*/
|
||||
private function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
|
||||
{
|
||||
// Check for goodwill voucher first (takes precedence)
|
||||
if (true === $participant->hasGoodwillVoucher && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
||||
return trim($participant->purchaseVoucherCode);
|
||||
}
|
||||
|
||||
// Fall back to promo voucher if present
|
||||
// Only return promotional voucher code (goodwill vouchers are handled separately)
|
||||
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
||||
return trim($participant->promoVoucherCode);
|
||||
}
|
||||
@@ -1219,6 +1231,21 @@ class BookingDataProcessor
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts goodwill voucher code for a participant.
|
||||
*
|
||||
* Goodwill vouchers are sent as <einloesecode> per participant (not in aggregate),
|
||||
* allowing them to coexist with promotional codes.
|
||||
*/
|
||||
private function getGoodwillVoucherCodeForParticipant(ParticipantDto $participant): ?string
|
||||
{
|
||||
if (true === $participant->hasGoodwillVoucher && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
||||
return trim($participant->purchaseVoucherCode);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies bulk insurance assignment if the applicant has enabled it.
|
||||
*
|
||||
|
||||
@@ -68,6 +68,11 @@ class Step3Controller extends AbstractController
|
||||
$form->handleRequest($request);
|
||||
|
||||
if (true === $form->isSubmitted() && true === $form->isValid()) {
|
||||
// Notify user if goodwill vouchers cannot be redeemed for inquiry bookings
|
||||
if ($bookingCreateDto->isInquiryBooking() && $bookingCreateDto->hasGoodwillVouchers()) {
|
||||
$this->addFlash('warning', 'Hinweis: Bei Anfragebuchungen können keine Kulanz-Gutscheine eingelöst werden. Kauf- und Aktionsgutscheine werden berücksichtigt.');
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate booking data with API by submitting an inquiry booking
|
||||
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
|
||||
|
||||
@@ -219,4 +219,52 @@ class BookingDto
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is an inquiry booking (status='A').
|
||||
*
|
||||
* Inquiry bookings occur when a travel is fully booked but still accepts inquiries.
|
||||
*/
|
||||
public function isInquiryBooking(): bool
|
||||
{
|
||||
return 'A' === $this->bookingStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any participants have entered voucher codes.
|
||||
*
|
||||
* @return bool True if any promotional, purchase, or goodwill vouchers are present
|
||||
*/
|
||||
public function hasVouchers(): bool
|
||||
{
|
||||
foreach ($this->participants as $participant) {
|
||||
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
||||
return true;
|
||||
}
|
||||
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any participants have entered goodwill vouchers.
|
||||
*
|
||||
* Goodwill vouchers (Kulanz) are a special type of purchase voucher that cannot be
|
||||
* redeemed in inquiry bookings. This method is used to display appropriate warnings.
|
||||
*
|
||||
* @return bool True if any goodwill vouchers are present
|
||||
*/
|
||||
public function hasGoodwillVouchers(): bool
|
||||
{
|
||||
foreach ($this->participants as $participant) {
|
||||
if (true === $participant->hasGoodwillVoucher) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<div class="booking-summary p-8 bg-gray-50 rounded-lg border sticky top-4">
|
||||
<h3 class="font-bold text-xl mb-6 text-gray-800">Buchungsübersicht</h3>
|
||||
|
||||
{# Inquiry booking notification #}
|
||||
{% if bookingCreateDto is defined and bookingCreateDto.isInquiryBooking() %}
|
||||
<div class="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded text-sm text-yellow-800">
|
||||
<span class="font-semibold">Anfragebuchung:</span> Diese Reise ist ausgebucht. Ihre Buchung wird als Anfrage verarbeitet.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Mutability information (edit mode only) #}
|
||||
{% if mutableData is defined %}
|
||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
||||
|
||||
@@ -8,7 +8,14 @@ use App\BusProNet\Model\Travel;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\ParticipantEditDto;
|
||||
use App\Service\BookingPriceCalculatorService;
|
||||
use App\Service\VoucherValidationService;
|
||||
use App\Validator\Constraints\PromoVoucherValidator;
|
||||
use App\Validator\Constraints\PurchaseVoucherValidator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
|
||||
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||
use Symfony\Component\Validator\Validation;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
@@ -25,8 +32,37 @@ class ParticipantEditDtoTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// Create mock services for validator dependencies
|
||||
$mockVoucherService = $this->createMock(VoucherValidationService::class);
|
||||
$mockPriceCalculatorService = $this->createMock(BookingPriceCalculatorService::class);
|
||||
|
||||
// Create custom validator factory that can inject dependencies
|
||||
$validatorFactory = new class($mockVoucherService, $mockPriceCalculatorService) implements ConstraintValidatorFactoryInterface {
|
||||
public function __construct(
|
||||
private readonly VoucherValidationService $voucherService,
|
||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getInstance(Constraint $constraint): ConstraintValidatorInterface
|
||||
{
|
||||
$className = $constraint->validatedBy();
|
||||
|
||||
if (PurchaseVoucherValidator::class === $className) {
|
||||
return new PurchaseVoucherValidator($this->voucherService);
|
||||
}
|
||||
|
||||
if (PromoVoucherValidator::class === $className) {
|
||||
return new PromoVoucherValidator($this->voucherService, $this->priceCalculatorService);
|
||||
}
|
||||
|
||||
return new $className();
|
||||
}
|
||||
};
|
||||
|
||||
$this->validator = Validation::createValidatorBuilder()
|
||||
->enableAttributeMapping()
|
||||
->setConstraintValidatorFactory($validatorFactory)
|
||||
->getValidator();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user