feat: fold purchase and promo voucher fields into one code field
This commit is contained in:
1 parent
93512b8345
commit
21d756a2e3
21 files changed
+801
-432
No files matched your search
@@ -16,6 +16,7 @@ use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
@@ -334,8 +335,8 @@ class BookingParticipantType extends AbstractType
|
||||
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
|
||||
$dynamicFields['bulkInsuranceBooking'] = CheckboxType::class;
|
||||
$dynamicFields['insurance'] = ChoiceType::class;
|
||||
$dynamicFields['purchaseVoucherCode'] = TextType::class;
|
||||
$dynamicFields['promoVoucherCode'] = TextType::class;
|
||||
$dynamicFields['voucherCode'] = TextType::class;
|
||||
$dynamicFields['removeVoucherCode'] = HiddenType::class;
|
||||
}
|
||||
|
||||
foreach ($dynamicFields as $fieldName => $fieldType) {
|
||||
|
||||
@@ -48,8 +48,8 @@ class ParticipantDto
|
||||
'licensePlate',
|
||||
'bulkInsuranceBooking',
|
||||
'insurance',
|
||||
'purchaseVoucherCode',
|
||||
'promoVoucherCode',
|
||||
'voucherCode',
|
||||
'removeVoucherCode',
|
||||
];
|
||||
|
||||
public ?int $index = null;
|
||||
@@ -163,47 +163,54 @@ class ParticipantDto
|
||||
public bool $bulkInsuranceBooking = false;
|
||||
|
||||
/**
|
||||
* Purchase voucher redemption code.
|
||||
* Collected from all participants and aggregated into single <gutscheine> collection in booking payload.
|
||||
* The voucher field as typed: a purchase or a promo code, told apart by VoucherCodeClassifier.
|
||||
* ParticipantVoucherFieldHandler applies it and clears it; a rejected code stays for correction.
|
||||
*/
|
||||
public ?string $voucherCode = null;
|
||||
|
||||
/**
|
||||
* Why the typed voucher code was rejected. Kept on the DTO because the htmx refresh rebuilds
|
||||
* the form from the DTO, which drops form errors; the Voucher constraint turns it into a violation.
|
||||
*/
|
||||
public ?string $voucherCodeError = null;
|
||||
|
||||
/**
|
||||
* Applied code the customer asked to remove. Consumed and reset by ParticipantVoucherFieldHandler.
|
||||
*/
|
||||
public ?string $removeVoucherCode = null;
|
||||
|
||||
/**
|
||||
* Applied purchase voucher redemption code (Kauf or Kulanz).
|
||||
* Kauf codes are aggregated into the booking's <gutscheine>, a Kulanz code is sent as the participant's <einloesecode>.
|
||||
*/
|
||||
#[Assert\Length(
|
||||
max: 50,
|
||||
maxMessage: 'Der Gutscheincode darf maximal {{ limit }} Zeichen lang sein'
|
||||
)]
|
||||
#[Assert\Regex(
|
||||
pattern: '/^[A-Za-z0-9\-]+$/',
|
||||
message: 'Der Gutscheincode darf nur Buchstaben, Zahlen und Bindestriche enthalten'
|
||||
)]
|
||||
public ?string $purchaseVoucherCode = null;
|
||||
|
||||
/**
|
||||
* Validated purchase voucher from API.
|
||||
* Set by ParticipantPurchaseVoucherFieldHandler during form processing.
|
||||
* Set by ParticipantVoucherFieldHandler during form processing.
|
||||
* Contains voucher number, remaining balance, and type (Kauf/Kulanz).
|
||||
*/
|
||||
public ?PurchaseVoucher $validatedPurchaseVoucher = null;
|
||||
|
||||
/**
|
||||
* Promo voucher code.
|
||||
* Applied promo voucher code.
|
||||
* Applied per participant in booking payload as <aktionscode>.
|
||||
*/
|
||||
#[Assert\Length(
|
||||
max: 50,
|
||||
maxMessage: 'Der Aktionscode darf maximal {{ limit }} Zeichen lang sein'
|
||||
)]
|
||||
#[Assert\Regex(
|
||||
pattern: '/^[A-Za-z0-9\-]+$/',
|
||||
message: 'Der Aktionscode darf nur Buchstaben, Zahlen und Bindestriche enthalten'
|
||||
)]
|
||||
public ?string $promoVoucherCode = null;
|
||||
|
||||
/**
|
||||
* Validated promo voucher from API.
|
||||
* Set by ParticipantPromoVoucherFieldHandler during form processing.
|
||||
* Set by ParticipantVoucherFieldHandler during form processing.
|
||||
* Contains discount amount/percentage and applicability (per person/booking).
|
||||
* Null while promoVoucherCode is set means BusPro rejected the applied code on a later check.
|
||||
*/
|
||||
public ?PromoVoucher $validatedPromoVoucher = null;
|
||||
|
||||
/**
|
||||
* Why BusPro rejected the applied promo code on a later check, e.g. the price fell below its threshold.
|
||||
*/
|
||||
public ?string $promoVoucherError = null;
|
||||
|
||||
/**
|
||||
* @var array<array{type: string, message: string}> Notification messages for user feedback
|
||||
*/
|
||||
@@ -345,8 +352,8 @@ class ParticipantDto
|
||||
/**
|
||||
* Checks if this participant has a goodwill (Kulanz) voucher.
|
||||
*
|
||||
* Goodwill vouchers are treated as promotional vouchers in BPN XML
|
||||
* (sent as <aktionscode> per participant, not in <gutscheine> collection).
|
||||
* Goodwill vouchers are sent as the participant's <einloesecode>,
|
||||
* not in the booking's <gutscheine> collection.
|
||||
*/
|
||||
public function hasGoodwillVoucher(): bool
|
||||
{
|
||||
@@ -425,6 +432,7 @@ class ParticipantDto
|
||||
$this->mobile = $this->normalizeNullableString($this->mobile);
|
||||
$this->remarksRoom = $this->normalizeNullableString($this->remarksRoom);
|
||||
$this->licensePlate = $this->normalizeNullableString($this->licensePlate);
|
||||
$this->voucherCode = $this->normalizeNullableString($this->voucherCode);
|
||||
$this->purchaseVoucherCode = $this->normalizeNullableString($this->purchaseVoucherCode);
|
||||
$this->promoVoucherCode = $this->normalizeNullableString($this->promoVoucherCode);
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ use function Symfony\Component\String\u;
|
||||
* the participant being edited and the full booking context needed for
|
||||
* cross-participant validation.
|
||||
*/
|
||||
#[AppAssert\PurchaseVoucher(groups: ['booking_create', 'booking_edit'])]
|
||||
#[AppAssert\PromoVoucher(groups: ['booking_create', 'booking_edit'])]
|
||||
#[AppAssert\MandatoryAdditionalServicesSelected(groups: ['booking_create', 'booking_edit'])]
|
||||
#[AppAssert\SkiPassSelection(groups: ['strict_required'])]
|
||||
class ParticipantEditDto
|
||||
@@ -71,6 +69,29 @@ class ParticipantEditDto
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports voucher codes BusPro rejected.
|
||||
*
|
||||
* ParticipantVoucherFieldHandler checks the codes with BusPro while handling the submission and
|
||||
* keeps the outcome on the participant, since the htmx refresh rebuilds the form and drops form
|
||||
* errors. This turns it into a violation so the step cannot be left with a rejected code.
|
||||
*/
|
||||
#[Assert\Callback(groups: ['booking_create', 'booking_edit'])]
|
||||
public function validateVoucherCodes(ExecutionContextInterface $context): void
|
||||
{
|
||||
if (null !== $this->participant->voucherCodeError) {
|
||||
$context->buildViolation($this->participant->voucherCodeError)
|
||||
->atPath('participant.voucherCode')
|
||||
->addViolation();
|
||||
}
|
||||
|
||||
if (null !== $this->participant->promoVoucherCode && null !== $this->participant->promoVoucherError) {
|
||||
$context->buildViolation($this->participant->promoVoucherError)
|
||||
->atPath('participant.voucherCode')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that non-applicant adult participants have unique email addresses.
|
||||
*
|
||||
|
||||
@@ -332,11 +332,11 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider
|
||||
// Vouchers cannot be redeemed or submitted for inquiry bookings
|
||||
$finalBookingOnlyCondition = new FinalBookingOnlyCondition();
|
||||
|
||||
$this->fieldStateConditions['purchaseVoucherCode'] = [
|
||||
$this->fieldStateConditions['voucherCode'] = [
|
||||
'hidden' => CompositeCondition::not($finalBookingOnlyCondition),
|
||||
];
|
||||
|
||||
$this->fieldStateConditions['promoVoucherCode'] = [
|
||||
$this->fieldStateConditions['removeVoucherCode'] = [
|
||||
'hidden' => CompositeCondition::not($finalBookingOnlyCondition),
|
||||
];
|
||||
|
||||
|
||||
@@ -263,6 +263,9 @@ class ParticipantFieldHandlerRegistry
|
||||
'dropOff',
|
||||
'parking',
|
||||
'licensePlate',
|
||||
// Cleared by the voucher handler once applied or removed; must not be bound again
|
||||
'voucherCode',
|
||||
'removeVoucherCode',
|
||||
];
|
||||
|
||||
foreach ($serviceFields as $fieldName) {
|
||||
|
||||
@@ -704,28 +704,22 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
];
|
||||
};
|
||||
|
||||
// Purchase voucher field provider - redemption code for vouchers that apply to complete booking
|
||||
// Collected from all participants and aggregated into single <gutscheine> collection in booking payload
|
||||
$this->fieldOptionProviders['purchaseVoucherCode'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Gutschein-Code',
|
||||
// Single voucher field - purchase (Kauf/Kulanz) or promo code, told apart by VoucherCodeClassifier.
|
||||
// Applied by ParticipantVoucherFieldHandler, which moves accepted codes to purchaseVoucherCode/promoVoucherCode
|
||||
$this->fieldOptionProviders['voucherCode'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Gutschein- oder Aktionscode',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'placeholder' => 'z.B. 1F7V6PADRZ',
|
||||
'placeholder' => 'z.B. 1F7V6PADRZ oder R-SOMMER',
|
||||
'maxlength' => 50,
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
'sanitize_html' => true,
|
||||
];
|
||||
|
||||
// Promo voucher field provider - promo code applied per participant in booking payload
|
||||
// Included in participant XML node as <aktionscode>
|
||||
$this->fieldOptionProviders['promoVoucherCode'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'label' => 'Rabatt-Code',
|
||||
// Applied voucher code to remove, set by the remove button of its chip
|
||||
$this->fieldOptionProviders['removeVoucherCode'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'placeholder' => 'z.B. LW0705',
|
||||
'maxlength' => 50,
|
||||
],
|
||||
'sanitize_html' => true,
|
||||
];
|
||||
|
||||
// Future field providers would be added here, for example:
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\BookingPriceCalculator;
|
||||
use App\Service\VoucherValidator;
|
||||
|
||||
/**
|
||||
* Handles promotional voucher code field processing.
|
||||
*
|
||||
* Validates promo codes via API and stores the validated voucher data
|
||||
* for early discount display. Validation is also performed by the
|
||||
* PromoVoucher constraint on ParticipantEditDto for form error feedback.
|
||||
*
|
||||
* Dependencies: ALL price-affecting fields (insurance, services, ski pass, rentals, etc.)
|
||||
* since promo validation requires accurate participant pricing.
|
||||
*/
|
||||
class ParticipantPromoVoucherFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VoucherValidator $voucherValidationService,
|
||||
private readonly BookingPriceCalculator $priceCalculatorService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'promoVoucherCode';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
// CRITICAL: Must run after ALL price-affecting handlers to ensure accurate pricing
|
||||
return [
|
||||
'dateOfBirth', // Required for age-based services
|
||||
'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
|
||||
'pickup', // Affects travel price
|
||||
'parking', // Affects travel price
|
||||
'insurance', // Affects travel price (must be last)
|
||||
];
|
||||
}
|
||||
|
||||
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||
{
|
||||
$promoCode = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
return null !== $promoCode && '' !== trim($promoCode);
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
if (null === $participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
$promoCode = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
// Clear validated voucher if code is empty
|
||||
if (null === $promoCode || '' === trim($promoCode)) {
|
||||
$participant->validatedPromoVoucher = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate participant price for validation
|
||||
$participantPrice = $this->priceCalculatorService->calculateIndividualParticipantPrice(
|
||||
$bookingDto,
|
||||
$participantIndex
|
||||
);
|
||||
|
||||
// Validate promo voucher and store result
|
||||
$result = $this->voucherValidationService->validatePromoVoucher(
|
||||
trim($promoCode),
|
||||
$bookingDto->travel->id,
|
||||
$participantPrice
|
||||
);
|
||||
|
||||
// Store validated voucher (null if invalid)
|
||||
$participant->validatedPromoVoucher = $result->isValid ? $result->voucher : null;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\VoucherValidator;
|
||||
|
||||
/**
|
||||
* Handles purchase voucher code field processing.
|
||||
*
|
||||
* Detects goodwill (Kulanz) vouchers and marks them for special XML handling.
|
||||
* Goodwill vouchers must be sent as <aktionscode> per participant, not aggregated
|
||||
* in <gutscheine> collection. Validation is performed by PurchaseVoucher constraint.
|
||||
*
|
||||
* Dependencies: None (purchase vouchers don't require price calculation)
|
||||
*/
|
||||
class ParticipantPurchaseVoucherFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VoucherValidator $voucherValidationService,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'purchaseVoucherCode';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
// No dependencies - purchase vouchers don't require price calculation
|
||||
return [];
|
||||
}
|
||||
|
||||
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||
{
|
||||
$voucherCode = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
return null !== $voucherCode && '' !== trim($voucherCode);
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
if (null === $participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
$voucherCode = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
|
||||
// Clear validated voucher if code is empty
|
||||
if (null === $voucherCode || '' === trim($voucherCode)) {
|
||||
$participant->validatedPurchaseVoucher = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate voucher and store result
|
||||
$result = $this->voucherValidationService->validatePurchaseVoucher($voucherCode);
|
||||
|
||||
// Store validated voucher (null if invalid)
|
||||
$participant->validatedPurchaseVoucher = $result->isValid ? $result->voucher : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Service;
|
||||
|
||||
use App\Enum\VoucherCodeKind;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||
use App\Service\BookingPriceCalculator;
|
||||
use App\Service\VoucherCodeClassifier;
|
||||
use App\Service\VoucherValidator;
|
||||
|
||||
/**
|
||||
* Handles the single voucher field of the booking create flow.
|
||||
*
|
||||
* The typed code is classified as promo or purchase code (VoucherCodeClassifier), checked with
|
||||
* BusPro and, when accepted, moved into promoVoucherCode or purchaseVoucherCode, which the payload
|
||||
* builder reads. The field is then cleared; a rejected code stays with its reason in voucherCodeError.
|
||||
* A participant holds at most one code of each kind.
|
||||
*
|
||||
* Runs on every create-mode submission, also without a typed code, because an applied promo code
|
||||
* is checked against the participant price, which may have changed since it was applied.
|
||||
*
|
||||
* Dependencies: ALL price-affecting fields (insurance, services, ski pass, rentals, etc.)
|
||||
* since promo validation requires accurate participant pricing.
|
||||
*/
|
||||
class ParticipantVoucherFieldHandler extends AbstractParticipantFieldHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly VoucherValidator $voucherValidationService,
|
||||
private readonly BookingPriceCalculator $priceCalculatorService,
|
||||
private readonly VoucherCodeClassifier $voucherCodeClassifier,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return 'voucherCode';
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
// CRITICAL: Must run after ALL price-affecting handlers to ensure accurate pricing
|
||||
return [
|
||||
'dateOfBirth', // Required for age-based services
|
||||
'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
|
||||
'pickup', // Affects travel price
|
||||
'parking', // Affects travel price
|
||||
'insurance', // Affects travel price (must be last)
|
||||
];
|
||||
}
|
||||
|
||||
public function shouldProcess(array $submittedData, string $mode, int $participantIndex): bool
|
||||
{
|
||||
// Vouchers can only be redeemed when creating a booking
|
||||
return BookingDto::MODE_CREATE === $mode;
|
||||
}
|
||||
|
||||
public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void
|
||||
{
|
||||
$participant = $this->getParticipant($bookingDto, $participantIndex);
|
||||
|
||||
if (!$participant instanceof ParticipantDto) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeRequestedCode($submittedData, $participant);
|
||||
$this->recheckAppliedPromoCode($bookingDto, $participant, $participantIndex);
|
||||
$this->applyTypedCode($submittedData, $bookingDto, $participant, $participantIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $submittedData
|
||||
*/
|
||||
private function removeRequestedCode(array $submittedData, ParticipantDto $participant): void
|
||||
{
|
||||
$participant->removeVoucherCode = null;
|
||||
|
||||
$code = $this->getFieldValue($submittedData, 'removeVoucherCode');
|
||||
if (false === is_string($code) || '' === trim($code)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$code = $this->voucherCodeClassifier->normalize($code);
|
||||
|
||||
if ($code === $participant->promoVoucherCode) {
|
||||
$participant->promoVoucherCode = null;
|
||||
$participant->validatedPromoVoucher = null;
|
||||
$participant->promoVoucherError = null;
|
||||
}
|
||||
|
||||
if ($code === $participant->purchaseVoucherCode) {
|
||||
$participant->purchaseVoucherCode = null;
|
||||
$participant->validatedPurchaseVoucher = null;
|
||||
}
|
||||
}
|
||||
|
||||
private function recheckAppliedPromoCode(BookingDto $bookingDto, ParticipantDto $participant, int $participantIndex): void
|
||||
{
|
||||
if (null === $participant->promoVoucherCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error = $this->validatePromoCode($participant->promoVoucherCode, $bookingDto, $participant, $participantIndex);
|
||||
$participant->promoVoucherError = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $submittedData
|
||||
*/
|
||||
private function applyTypedCode(array $submittedData, BookingDto $bookingDto, ParticipantDto $participant, int $participantIndex): void
|
||||
{
|
||||
$participant->voucherCodeError = null;
|
||||
|
||||
// Only a submitted field carries a typed code; when the field is hidden nothing may linger
|
||||
$typedCode = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||
if (false === is_string($typedCode) || '' === trim($typedCode)) {
|
||||
$participant->voucherCode = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$participant->voucherCode = trim($typedCode);
|
||||
$code = $this->voucherCodeClassifier->normalize($typedCode);
|
||||
|
||||
$error = match (true) {
|
||||
$code === $participant->promoVoucherCode, $code === $participant->purchaseVoucherCode => 'Dieser Code ist bereits eingelöst.',
|
||||
default => match ($this->voucherCodeClassifier->classify($code)) {
|
||||
VoucherCodeKind::Malformed => 'Dieser Code ist ungültig. Bitte prüfe deine Eingabe.',
|
||||
VoucherCodeKind::Promo => $this->applyPromoCode($code, $bookingDto, $participant, $participantIndex),
|
||||
VoucherCodeKind::Purchase => $this->applyPurchaseCode($code, $participant),
|
||||
},
|
||||
};
|
||||
|
||||
if (null === $error) {
|
||||
$participant->voucherCode = null;
|
||||
}
|
||||
|
||||
$participant->voucherCodeError = $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null Why the code was not applied, or null when it was
|
||||
*/
|
||||
private function applyPromoCode(string $code, BookingDto $bookingDto, ParticipantDto $participant, int $participantIndex): ?string
|
||||
{
|
||||
if (null !== $participant->promoVoucherCode) {
|
||||
return sprintf('Es kann nur ein Aktionscode eingelöst werden. Bitte entferne zuerst %s.', $participant->promoVoucherCode);
|
||||
}
|
||||
|
||||
$error = $this->validatePromoCode($code, $bookingDto, $participant, $participantIndex);
|
||||
if (null !== $error) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$participant->promoVoucherCode = $code;
|
||||
$participant->promoVoucherError = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null Why the code was not applied, or null when it was
|
||||
*/
|
||||
private function applyPurchaseCode(string $code, ParticipantDto $participant): ?string
|
||||
{
|
||||
if (null !== $participant->purchaseVoucherCode) {
|
||||
return sprintf('Es kann nur ein Gutschein eingelöst werden. Bitte entferne zuerst %s.', $participant->purchaseVoucherCode);
|
||||
}
|
||||
|
||||
$result = $this->voucherValidationService->validatePurchaseVoucher($code);
|
||||
if (false === $result->isValid) {
|
||||
return sprintf('Gutschein ungültig: %s', $result->errorMessage ?? 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
$participant->purchaseVoucherCode = $code;
|
||||
$participant->validatedPurchaseVoucher = $result->voucher;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a promo code against the participant's current price and stores the validated voucher.
|
||||
*
|
||||
* @return string|null Why BusPro rejected the code, or null when it is valid
|
||||
*/
|
||||
private function validatePromoCode(string $code, BookingDto $bookingDto, ParticipantDto $participant, int $participantIndex): ?string
|
||||
{
|
||||
$travelId = $bookingDto->travel->id;
|
||||
if (null === $travelId) {
|
||||
$participant->validatedPromoVoucher = null;
|
||||
|
||||
return 'Aktionscode konnte nicht validiert werden: Reise-ID fehlt.';
|
||||
}
|
||||
|
||||
$participantPrice = $this->priceCalculatorService->calculateIndividualParticipantPrice($bookingDto, $participantIndex);
|
||||
|
||||
$result = $this->voucherValidationService->validatePromoVoucher($code, $travelId, $participantPrice);
|
||||
|
||||
if (false === $result->isValid) {
|
||||
// Only an applied code is replaced: a rejected new code must not discard the applied one's discount
|
||||
if ($code === $participant->promoVoucherCode) {
|
||||
$participant->validatedPromoVoucher = null;
|
||||
}
|
||||
|
||||
return sprintf('Aktionscode ungültig: %s', $result->errorMessage ?? 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
$participant->validatedPromoVoucher = $result->voucher;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user