, as the live shop does. * * Step 2 is validated with the groups ['voucher_step_2', 'personal_data']: the buyer address reuses * the Address model, whose constraints live in the personal_data group. */ class VoucherPurchaseDto { public const string SALUTATION_MR = 'Herr'; public const string SALUTATION_MS = 'Frau'; public int $currentStep = 1; // BusPro's bounds from GUTSCHEINVORLAGEN at the start of the flow. BusPro re-checks them on // every GUTSCHEINBUCHUNG (607/608), so a change in between still surfaces as a step 1 error. public int $minAmount = 0; public int $maxAmount = 0; // Fixed at the start of the flow from GUTSCHEINZAHLUNGSARTEN; transfer is the only one offered. public ?VoucherPaymentMethod $paymentMethod = null; // Step 1: voucher #[Assert\NotNull(message: 'Bitte wähle ein Motiv', groups: ['voucher_step_1'])] public ?VoucherTemplate $template = null; #[Assert\NotBlank(message: 'Bitte angeben', groups: ['voucher_step_1'])] public ?string $from = null; #[Assert\NotBlank(message: 'Bitte angeben', groups: ['voucher_step_1'])] public ?string $to = null; #[Assert\NotNull(message: 'Bitte gib einen Gutscheinwert an', groups: ['voucher_step_1'])] #[Assert\Range( notInRangeMessage: 'Der Gutscheinwert muss zwischen {{ min }} und {{ max }} Euro liegen', minPropertyPath: 'minAmount', maxPropertyPath: 'maxAmount', groups: ['voucher_step_1'], )] public ?int $amount = null; // Step 2: buyer (rechnungsempfänger) #[Assert\Choice(choices: [self::SALUTATION_MR, self::SALUTATION_MS], groups: ['voucher_step_2'])] public ?string $salutation = null; public ?string $title = null; #[Assert\NotBlank(message: 'Bitte angeben', groups: ['voucher_step_2'])] public ?string $lastName = null; #[Assert\NotBlank(message: 'Bitte angeben', groups: ['voucher_step_2'])] public ?string $firstName = null; #[Assert\Valid] public Address $address; #[Assert\When( expression: 'this.isPhoneNewlyEntered()', constraints: [new AppAssert\PhoneNumber()], groups: ['voucher_step_2'], )] public ?string $phone = null; #[Assert\When( expression: 'this.isMobileNewlyEntered()', constraints: [new AppAssert\PhoneNumber()], groups: ['voucher_step_2'], )] public ?string $mobile = null; // The numbers prefilled from BusPro, which are exempt from PhoneNumber. public ?string $loadedPhone = null; public ?string $loadedMobile = null; #[Assert\NotBlank(message: 'Bitte angeben', groups: ['voucher_step_2'])] #[Assert\Email(message: 'Bitte eine gültige Adresse angeben', mode: 'strict', groups: ['voucher_step_2'])] public ?string $email = null; // Step 3: confirmation, reset on every visit so a stale tick never carries over #[Assert\IsTrue(message: 'Bitte bestätige', groups: ['voucher_step_3'])] public bool $termsAccepted = false; #[Assert\IsTrue(message: 'Bitte bestätige', groups: ['voucher_step_3'])] public bool $purchaseConfirmed = false; public function __construct() { $this->address = new Address(); $this->address->country = 'D'; } public function isPhoneNewlyEntered(): bool { return AppAssert\PhoneNumber::isNewlyEntered($this->phone, $this->loadedPhone); } public function isMobileNewlyEntered(): bool { return AppAssert\PhoneNumber::isNewlyEntered($this->mobile, $this->loadedMobile); } public function getBuyerFullName(): string { return trim(sprintf('%s %s', $this->firstName ?? '', $this->lastName ?? '')); } public function hasBuyerData(): bool { return null !== $this->lastName && '' !== $this->lastName; } /** * Copies the buyer fields from the signed-in account's BusPro record. * * The e-mail is the one the account signed in with, not the record's communication address: * a BusPro address can carry several e-mails for different persons. */ public function prefillBuyer(PersonalData $personalData, ?string $loginEmail): void { $this->salutation = \in_array($personalData->salutation, [self::SALUTATION_MR, self::SALUTATION_MS], true) ? $personalData->salutation : null; $this->title = $personalData->title; $this->lastName = $personalData->name; $this->firstName = $personalData->firstName; $this->address = clone $personalData->address; $this->address->normalize(); $this->address->country ??= 'D'; $this->phone = $personalData->communication->phone; $this->mobile = $personalData->communication->mobile; $this->loadedPhone = $this->phone; $this->loadedMobile = $this->mobile; $this->email = $loginEmail; } }