feat: correct handling of vouchers
This commit is contained in:
@@ -204,7 +204,7 @@ class ApiClient
|
|||||||
'user' => $this->config['bpn_username'],
|
'user' => $this->config['bpn_username'],
|
||||||
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
|
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
|
||||||
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
|
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
|
||||||
'buchungsart' => 'Buchung',
|
'buchungsart' => Constants::BOOKING_TYPE_BOOKING,
|
||||||
...$payload,
|
...$payload,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class ApiClient
|
|||||||
{
|
{
|
||||||
// Forcibly override booking status to generate inquiry payload
|
// Forcibly override booking status to generate inquiry payload
|
||||||
$bookingDto->bookingStatus = 'A';
|
$bookingDto->bookingStatus = 'A';
|
||||||
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, 'Anfrage');
|
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_INQUIRY);
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'user' => $this->config['bpn_username'],
|
'user' => $this->config['bpn_username'],
|
||||||
@@ -255,7 +255,7 @@ class ApiClient
|
|||||||
*/
|
*/
|
||||||
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
|
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 = [
|
$data = [
|
||||||
'user' => $this->config['bpn_username'],
|
'user' => $this->config['bpn_username'],
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ final class Constants
|
|||||||
public const STATUS_BLOCKED = 'Buchungsstop';
|
public const STATUS_BLOCKED = 'Buchungsstop';
|
||||||
public const STATUS_ON_REQUEST = 'Anfrage';
|
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
|
// Payment methods
|
||||||
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
public const PAYMENT_METHOD_TRANSFER = 'transfer';
|
||||||
public const PAYMENT_METHOD_DEBIT = 'debit';
|
public const PAYMENT_METHOD_DEBIT = 'debit';
|
||||||
|
|||||||
@@ -688,11 +688,16 @@ class BookingDataProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add promo voucher or goodwill voucher as aktionscode
|
// Add promotional voucher as promotionalCode
|
||||||
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
$promotionalCode = $this->getPromoVoucherCodeForParticipant($dto);
|
||||||
$aktionscode = $this->getPromoVoucherCodeForParticipant($dto);
|
if (null !== $promotionalCode) {
|
||||||
if (null !== $aktionscode) {
|
$participantPayload['aktionscode'] = $promotionalCode;
|
||||||
$participantPayload['aktionscode'] = $aktionscode;
|
}
|
||||||
|
|
||||||
|
// 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
|
// Add participants
|
||||||
$payload['teilnehmerliste']['teilnehmer'] = [];
|
$payload['teilnehmerliste']['teilnehmer'] = [];
|
||||||
foreach ($bookingDto->participants as $index => $participant) {
|
foreach ($bookingDto->participants as $index => $participant) {
|
||||||
@@ -885,11 +893,19 @@ class BookingDataProcessor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add promo voucher or goodwill voucher as aktionscode
|
// Add promotional voucher as aktionscode (allowed for inquiry bookings)
|
||||||
// Goodwill vouchers (Kulanz) take precedence over promo vouchers
|
$promotionalCode = $this->getPromoVoucherCodeForParticipant($participant);
|
||||||
$aktionscode = $this->getPromoVoucherCodeForParticipant($participant);
|
if (null !== $promotionalCode) {
|
||||||
if (null !== $aktionscode) {
|
$participantData['aktionscode'] = $promotionalCode;
|
||||||
$participantData['aktionscode'] = $aktionscode;
|
}
|
||||||
|
|
||||||
|
// 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;
|
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
|
||||||
@@ -909,7 +925,9 @@ class BookingDataProcessor
|
|||||||
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
|
||||||
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
|
$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);
|
$purchaseVouchers = $this->collectPurchaseVouchers($bookingDto);
|
||||||
if (false === empty($purchaseVouchers)) {
|
if (false === empty($purchaseVouchers)) {
|
||||||
$payload['gutscheine']['gutschein'] = [];
|
$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.
|
* Returns the promotional voucher code if present.
|
||||||
* If yes, returns that code (goodwill vouchers override promo vouchers).
|
* Goodwill vouchers are handled separately via getGoodwillVoucherCodeForParticipant().
|
||||||
* Otherwise, returns the promo voucher code if present.
|
|
||||||
*
|
*
|
||||||
* @return string|null The aktionscode to use, or null if none
|
* @return string|null The aktionscode to use, or null if none
|
||||||
*/
|
*/
|
||||||
private function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
|
private function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
|
||||||
{
|
{
|
||||||
// Check for goodwill voucher first (takes precedence)
|
// Only return promotional voucher code (goodwill vouchers are handled separately)
|
||||||
if (true === $participant->hasGoodwillVoucher && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
|
|
||||||
return trim($participant->purchaseVoucherCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to promo voucher if present
|
|
||||||
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
|
||||||
return trim($participant->promoVoucherCode);
|
return trim($participant->promoVoucherCode);
|
||||||
}
|
}
|
||||||
@@ -1219,6 +1231,21 @@ class BookingDataProcessor
|
|||||||
return null;
|
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.
|
* Applies bulk insurance assignment if the applicant has enabled it.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ class Step3Controller extends AbstractController
|
|||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if (true === $form->isSubmitted() && true === $form->isValid()) {
|
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 {
|
try {
|
||||||
// Validate booking data with API by submitting an inquiry booking
|
// Validate booking data with API by submitting an inquiry booking
|
||||||
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
|
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
|
||||||
|
|||||||
@@ -219,4 +219,52 @@ class BookingDto
|
|||||||
->addViolation();
|
->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">
|
<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>
|
<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) #}
|
{# Mutability information (edit mode only) #}
|
||||||
{% if mutableData is defined %}
|
{% if mutableData is defined %}
|
||||||
<div class="mb-6 pb-4 border-b border-gray-200">
|
<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\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Model\ParticipantEditDto;
|
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 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\Validation;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
@@ -25,8 +32,37 @@ class ParticipantEditDtoTest extends TestCase
|
|||||||
|
|
||||||
protected function setUp(): void
|
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()
|
$this->validator = Validation::createValidatorBuilder()
|
||||||
->enableAttributeMapping()
|
->enableAttributeMapping()
|
||||||
|
->setConstraintValidatorFactory($validatorFactory)
|
||||||
->getValidator();
|
->getValidator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user