Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*.swp
*.swo
/.idea
.DS_Store
20 changes: 20 additions & 0 deletions src/Actions/Checkout/VerifyAvailabilityAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ public function run(CheckoutParamsDto $params, ItinerarySummary $itinerary): boo
{
$dto = $this->getVerifyAvailabilityResponse($params);

return $this->applyAvailabilityResponse($dto, $itinerary);
}

/**
* Verify availability and return both the bookable result and on-request status.
*
* @return array{bookable: bool, isOnRequest: bool}
*/
public function runWithSummary(CheckoutParamsDto $params, ItinerarySummary $itinerary): array
{
$dto = $this->getVerifyAvailabilityResponse($params);

return [
'bookable' => $this->applyAvailabilityResponse($dto, $itinerary),
'isOnRequest' => $dto->summary->isOnRequest,
];
}

private function applyAvailabilityResponse(VerifyAvailabilityResponse $dto, ItinerarySummary $itinerary): bool
{
/** @var Collection<int, bool> $statuses */
$statuses = new Collection;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function __construct(
public bool $nonBookable,
public bool $bookingWindowEnd,
public PriceResponse $prices,
public array $remarks = []
public array $remarks = [],
public bool $isOnRequest = false,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities;

use Nezasa\Checkout\Dtos\BaseDto;

class OnRequestResponseEntity extends BaseDto
{
/**
* Create a new instance of the OnRequestResponseEntity.
*/
public function __construct(
public bool $confirmationEnabled = false,
public ?string $confirmationText = null,
public ?string $remarks = null,
) {}

/**
* Get a stable key for the on-request acceptance based on displayed content.
*/
public function getConfirmationKey(): string
{
return md5(json_encode([
'confirmationText' => $this->confirmationText,
'remarks' => $this->remarks,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Nezasa\Checkout\Dtos\BaseDto;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\EuPrrlResponseEntity;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\OnRequestResponseEntity;

class RegulatoryInformationResponse extends BaseDto
{
Expand All @@ -17,8 +18,11 @@ class RegulatoryInformationResponse extends BaseDto
public function __construct(
public ?string $paymentExplainer = null,
public ?EuPrrlResponseEntity $euPrrl = null,
public ?OnRequestResponseEntity $onRequest = null,

) {}
) {
$this->onRequest = new OnRequestResponseEntity;
}

/**
* Determine whether EU-PRRL package compliance should block checkout.
Expand Down
71 changes: 71 additions & 0 deletions src/Livewire/PaymentOptionsSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

use Illuminate\Contracts\View\View;
use Livewire\Attributes\On;
use Livewire\Attributes\Reactive;
use Nezasa\Checkout\Actions\Checkout\GetPaymentProviderAction;
use Nezasa\Checkout\Dtos\View\PaymentOption;
use Nezasa\Checkout\Enums\Section;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\OnRequestResponseEntity;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\PriceResponse;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\RegulatoryInformationResponse;

Expand All @@ -29,6 +31,22 @@ class PaymentOptionsSection extends BaseCheckoutComponent
*/
public PriceResponse $price;

/**
* Whether the latest availability check marked the itinerary as on-request.
*/
#[Reactive]
public bool $isOnRequest = false;

/**
* Whether the customer accepted the on-request confirmation.
*/
public bool $acceptedOnRequestTerms = false;

/**
* Whether to show the on-request confirmation validation error.
*/
public bool $showOnRequestTermsError = false;

/**
* Create a new instance of the component.
*/
Expand All @@ -39,6 +57,8 @@ public function mount(GetPaymentProviderAction $getPaymentProviderAction): void
if ($this->model->rest_payment) {
$this->isExpanded = true;
}

$this->acceptedOnRequestTerms = $this->hasAcceptedOnRequestTerms();
}

/**
Expand All @@ -50,6 +70,57 @@ public function render(): View
return view('checkout::blades.payment-options-section');
}

public function requiresOnRequestConfirmation(): bool
{
return $this->isOnRequest
&& isset($this->regulatoryInformation)
&& $this->regulatoryInformation->onRequest?->confirmationEnabled === true;
}

/**
* Persist and validate the on-request confirmation checkbox.
*/
public function toggleOnRequestTerms(bool $value): void
{
$this->acceptedOnRequestTerms = $value;

if ($this->regulatoryInformation->onRequest instanceof OnRequestResponseEntity) {
$this->model->updateData([
'acceptedTerms.'.$this->regulatoryInformation->onRequest->getConfirmationKey() => $value,
]);
}

$this->showOnRequestTermsError = $value === false && $this->requiresOnRequestConfirmation();

$this->dispatch('on-request-confirmation-updated', accepted: $value);
}

#[On('on-request-confirmation-required')]
public function showOnRequestConfirmationError(): void
{
if (! $this->requiresOnRequestConfirmation()) {
return;
}

$this->showOnRequestTermsError = true;
}

private function hasAcceptedOnRequestTerms(): bool
{
if (! isset($this->regulatoryInformation)) {
return false;
}

if (! $this->regulatoryInformation->onRequest instanceof OnRequestResponseEntity) {
return false;
}

$data = json_decode(json_encode($this->model->data, JSON_THROW_ON_ERROR), true, flags: JSON_THROW_ON_ERROR);
$acceptedTerms = is_array($data) ? data_get($data, 'acceptedTerms', []) : [];

return ($acceptedTerms[$this->regulatoryInformation->onRequest->getConfirmationKey()] ?? false) === true;
}

/**
* Listen for the 'traveller-processed' event to determine if the promo code section should be expanded or completed.
*/
Expand Down
53 changes: 52 additions & 1 deletion src/Livewire/TripDetailsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Nezasa\Checkout\Dtos\Planner\ItinerarySummary;
use Nezasa\Checkout\Dtos\Planner\RequiredResponses;
use Nezasa\Checkout\Enums\Section;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\Entities\OnRequestResponseEntity;
use Nezasa\Checkout\Integrations\Nezasa\Dtos\Responses\PriceResponse;
use Throwable;
use URL;
Expand Down Expand Up @@ -45,6 +46,11 @@ class TripDetailsPage extends BaseCheckoutComponent
*/
public RequiredResponses $result;

/**
* Indicates whether the latest availability check marked the itinerary as on-request.
*/
public bool $isOnRequest = false;

public function mount(
CallTripDetailsAction $callTripDetails,
FindCheckoutModelAction $findCheckoutModelAction,
Expand Down Expand Up @@ -99,6 +105,7 @@ public function render(): View
public function createPaymentPageUrl(string $gateway): void
{
$this->gateway = $gateway;
$this->paymentPageUrl = null;

if ($this->model->rest_payment) {
$this->generatePaymentPageUrl(result: true);
Expand Down Expand Up @@ -127,8 +134,18 @@ public function createPaymentPageUrl(string $gateway): void
}

#[On('availability-verified')]
public function generatePaymentPageUrl(bool $result): void
public function generatePaymentPageUrl(bool $result, bool $isOnRequest = false): void
{
$this->isOnRequest = $isOnRequest;

if ($result && $this->requiresOnRequestConfirmation() && ! $this->hasAcceptedOnRequestTerms()) {
$this->checkingAvailability = false;
$this->paymentPageUrl = null;
$this->dispatch('on-request-confirmation-required');

return;
}

if ($result) {
$this->paymentPageUrl = URL::temporarySignedRoute(
name: 'payment',
Expand All @@ -143,6 +160,40 @@ public function generatePaymentPageUrl(bool $result): void
$this->checkingAvailability = false;
}

#[On('on-request-confirmation-updated')]
public function onRequestConfirmationUpdated(bool $accepted): void
{
if ($accepted) {
if ($this->gateway !== null && $this->isOnRequest) {
$this->generatePaymentPageUrl(result: true, isOnRequest: true);
}

return;
}

$this->paymentPageUrl = null;
}

public function requiresOnRequestConfirmation(): bool
{
return $this->isOnRequest
&& $this->result->regulatoryInformation->onRequest?->confirmationEnabled === true;
}

private function hasAcceptedOnRequestTerms(): bool
{
if (! $this->result->regulatoryInformation->onRequest instanceof OnRequestResponseEntity) {
return false;
}

$this->model->refresh();

$data = json_decode(json_encode($this->model->data, JSON_THROW_ON_ERROR), true, flags: JSON_THROW_ON_ERROR);
$acceptedTerms = is_array($data) ? data_get($data, 'acceptedTerms', []) : [];

return ($acceptedTerms[$this->result->regulatoryInformation->onRequest->getConfirmationKey()] ?? false) === true;
}

/**
* Handle the promo code applied event.
*
Expand Down
5 changes: 4 additions & 1 deletion src/Livewire/TripSummary.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ public function summaryUpdated(): void
#[On('payment-selected')]
public function verifyAvailability(): void
{
$availability = resolve(VerifyAvailabilityAction::class)->runWithSummary($this->getParams(), $this->itinerary);

$this->dispatch(
event: 'availability-verified',
result: resolve(VerifyAvailabilityAction::class)->run($this->getParams(), $this->itinerary),
result: $availability['bookable'],
isOnRequest: $availability['isOnRequest'],
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Resources/Views/blades/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
:$model
:price="$itinerary->price"
:$regulatoryInformation
:is-on-request="$isOnRequest"
:is-completed="$model->isCompleted(Section::PaymentOptions)"
:is-expanded="$model->isExpanded(Section::PaymentOptions)"
/>
Expand Down
37 changes: 37 additions & 0 deletions src/Resources/Views/blades/payment-options-section.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,43 @@ class="peer sr-only"
</div>
</div>

@if($this->requiresOnRequestConfirmation() && $regulatoryInformation->onRequest !== null)
<div class="space-y-3">
@if(filled($regulatoryInformation->onRequest->remarks))
<div class="text-sm leading-relaxed text-gray-600
[&_a]:text-blue-600
[&_a]:underline
[&_a:hover]:text-blue-700">
{!! $regulatoryInformation->onRequest->remarks !!}
</div>
@endif

<div class="rounded-md p-6 mt-4 border
@if($showOnRequestTermsError)
border-red-500 @else border-gray-300 @endif">

<label class="flex items-center space-x-3 cursor-pointer">
<input
type="checkbox"
@checked($acceptedOnRequestTerms)
wire:change="toggleOnRequestTerms($event.target.checked)"
class="h-5 w-5 text-blue-600 border-gray-300 rounded">

<span class="text-sm inline text-gray-600
[&_a]:text-blue-600
[&_a]:underline
[&_a:hover]:text-blue-700">
{!! $regulatoryInformation->onRequest->confirmationText !!}
</span>
</label>

@if($showOnRequestTermsError)
<p class="text-red-600 text-sm mt-2">{{ trans('checkout::input.validations.agree_to_continue') }}</p>
@endif
</div>
</div>
@endif

</div>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"prrl.requirementsNotFulfilled.generic"
]
}
},
"onRequest": {
"confirmationEnabled": false,
"confirmationText": null,
"remarks": null
}
}
}
5 changes: 5 additions & 0 deletions tests/Fixtures/Saloon/regulatory_information_response.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"compliant": true,
"reasons": []
}
},
"onRequest": {
"confirmationEnabled": false,
"confirmationText": null,
"remarks": null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"prrl.requirementsNotFulfilled.generic"
]
}
},
"onRequest": {
"confirmationEnabled": false,
"confirmationText": null,
"remarks": null
}
}
}
Loading
Loading