Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAYOSWXP-158: Add PayPal v2 payment methods #331

Merged
merged 14 commits into from
Jan 13, 2025
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
125 changes: 81 additions & 44 deletions src/Components/GenericExpressCheckout/CustomerRegistrationUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,115 @@

namespace PayonePayment\Components\GenericExpressCheckout;

use PayonePayment\Core\Utils\AddressCompare;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Validation\DataBag\RequestDataBag;
use Shopware\Core\Framework\Validation\DataValidationFactoryInterface;
use Shopware\Core\Framework\Validation\DataValidator;
use Shopware\Core\System\Country\CountryEntity;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\Salutation\SalutationEntity;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\TranslatorInterface;

class CustomerRegistrationUtil
{
public function __construct(
private readonly EntityRepository $salutationRepository,
private readonly EntityRepository $countryRepository,
private readonly TranslatorInterface $translator
private readonly TranslatorInterface $translator,
private readonly DataValidationFactoryInterface $addressValidationFactory,
private readonly DataValidator $validator,
private readonly LoggerInterface $logger
) {
}

public function getCustomerDataBagFromGetCheckoutSessionResponse(array $response, Context $context): RequestDataBag
public function getCustomerDataBagFromGetCheckoutSessionResponse(array $response, SalesChannelContext $salesChannelContext): RequestDataBag
{
$salutationId = $this->getSalutationId($context);
$salutationId = $this->getSalutationId($salesChannelContext->getContext());

$billingAddress = [
'salutationId' => $salutationId,
'company' => $this->extractBillingData($response, 'company'),
'firstName' => $this->extractBillingData($response, 'firstname'),
'lastName' => $this->extractBillingData($response, 'lastname'),
'street' => $this->extractBillingData($response, 'street'),
'additionalAddressLine1' => $this->extractBillingData($response, 'addressaddition'),
'zipcode' => $this->extractBillingData($response, 'zip'),
'city' => $this->extractBillingData($response, 'city'),
'countryId' => $this->getCountryIdByCode($this->extractBillingData($response, 'country') ?? '', $salesChannelContext->getContext()),
'phone' => $this->extractBillingData($response, 'telephonenumber'),
];

$shippingAddress = [
'salutationId' => $salutationId,
'company' => $this->extractShippingData($response, 'company'),
'firstName' => $this->extractShippingData($response, 'firstname'),
'lastName' => $this->extractShippingData($response, 'lastname'),
'street' => $this->extractShippingData($response, 'street'),
'additionalAddressLine1' => $this->extractShippingData($response, 'addressaddition'),
'zipcode' => $this->extractShippingData($response, 'zip'),
'city' => $this->extractShippingData($response, 'city'),
'countryId' => $this->getCountryIdByCode($this->extractShippingData($response, 'country') ?? '', $salesChannelContext->getContext()),
'phone' => $this->extractShippingData($response, 'telephonenumber'),
];

$billingAddressViolations = $this->validateAddress($billingAddress, $salesChannelContext);
$shippingAddressViolations = $this->validateAddress($shippingAddress, $salesChannelContext);

$isBillingAddressComplete = $billingAddressViolations->count() === 0;
$isShippingAddressComplete = $shippingAddressViolations->count() === 0;

if (!$isBillingAddressComplete && !$isShippingAddressComplete) {
$this->logger->error('PAYONE Express Checkout: The delivery and billing address is incomplete', [
'billingAddress' => $billingAddress,
'shippingAddress' => $shippingAddress,
'billingAddressViolations' => $billingAddressViolations->__toString(),
'shippingAddressViolations' => $shippingAddressViolations->__toString(),
]);

throw new RuntimeException($this->translator->trans('PayonePayment.errorMessages.genericError'));
rommelfreddy marked this conversation as resolved.
Show resolved Hide resolved
}

if (!$isBillingAddressComplete && $isShippingAddressComplete) {
$billingAddress = $shippingAddress;
}

$customerData = new RequestDataBag([
'guest' => true,
'salutationId' => $salutationId,
'email' => $response['addpaydata']['email'],
'firstName' => $this->extractBillingData($response, 'firstname'),
'lastName' => $this->extractBillingData($response, 'lastname'),
'firstName' => $billingAddress['firstName'],
'lastName' => $billingAddress['lastName'],
'acceptedDataProtection' => true,
'billingAddress' => array_filter([
'salutationId' => $salutationId,
'company' => $this->extractBillingData($response, 'company'),
'firstName' => $this->extractBillingData($response, 'firstname'),
'lastName' => $this->extractBillingData($response, 'lastname'),
'street' => $this->extractBillingData($response, 'street'),
'additionalAddressLine1' => $this->extractBillingData($response, 'addressaddition'),
'zipcode' => $this->extractBillingData($response, 'zip'),
'city' => $this->extractBillingData($response, 'city'),
'countryId' => $this->getCountryIdByCode($this->extractBillingData($response, 'country') ?? '', $context),
'phone' => $this->extractBillingData($response, 'telephonenumber'),
]),
'shippingAddress' => array_filter([
'salutationId' => $salutationId,
'company' => $this->extractShippingData($response, 'company'),
'firstName' => $this->extractShippingData($response, 'firstname'),
'lastName' => $this->extractShippingData($response, 'lastname'),
'street' => $this->extractShippingData($response, 'street'),
'additionalAddressLine1' => $this->extractShippingData($response, 'addressaddition'),
'zipcode' => $this->extractShippingData($response, 'zip'),
'city' => $this->extractShippingData($response, 'city'),
'countryId' => $this->getCountryIdByCode($this->extractShippingData($response, 'country') ?? '', $context),
'phone' => $this->extractShippingData($response, 'telephonenumber'),
]),
'billingAddress' => $billingAddress,
'shippingAddress' => $shippingAddress,
]);

if ($this->extractBillingData($response, 'company') !== null) {
if ($customerData->get('billingAddress')?->get('company') !== null) {
$customerData->set('accountType', CustomerEntity::ACCOUNT_TYPE_BUSINESS);
} else {
$customerData->set('accountType', CustomerEntity::ACCOUNT_TYPE_PRIVATE);
}

$billingAddress = $customerData->get('billingAddress')?->all() ?: [];
$shippingAddress = $customerData->get('shippingAddress')?->all() ?: [];
if (array_diff($billingAddress, $shippingAddress) === []) {
if (!$isShippingAddressComplete || AddressCompare::areRawAddressesIdentical($billingAddress, $shippingAddress)) {
$customerData->remove('shippingAddress');
}

return $customerData;
}

private function extractBillingData(array $response, string $key, string|null $alternateKey = null): ?string
momocode-de marked this conversation as resolved.
Show resolved Hide resolved
private function extractBillingData(array $response, string $key): ?string
{
// special case: PayPal express: PayPal does not return firstname. so we need to take the firstname from the shipping-data
// special case: PayPal v1 express: PayPal does not return firstname. so we need to take the firstname from the shipping-data
if (($key === 'firstname' || $key === 'lastname')
&& !\array_key_exists('firstname', $response['addpaydata'])
&& isset(
Expand All @@ -92,20 +126,16 @@ private function extractBillingData(array $response, string $key, string|null $a
}
}

if ($alternateKey === null
rommelfreddy marked this conversation as resolved.
Show resolved Hide resolved
&& !\array_key_exists('billing_lastname', $response['addpaydata'])
&& !\array_key_exists('lastname', $response['addpaydata'])
) {
// there are no explicit billing-address-details. We assume that there are only shipping details. So we use the shipping details for the billing details too.
$alternateKey = 'shipping_' . $key;
}

return $response['addpaydata']['billing_' . $key] ?? $response['addpaydata'][$key] ?? ($alternateKey ? $response['addpaydata'][$alternateKey] : null);
// Do not take any values from the shipping address as a fallback for individual fields.
// If mandatory fields are missing from the billing address, the complete shipping address is used
return $response['addpaydata']['billing_' . $key] ?? $response['addpaydata'][$key] ?? null;
}

private function extractShippingData(array $response, string $key, ?string $alternateKey = null): ?string
momocode-de marked this conversation as resolved.
Show resolved Hide resolved
private function extractShippingData(array $response, string $key): ?string
{
return $response['addpaydata']['shipping_' . $key] ?? $response['addpaydata'][$key] ?? $response['addpaydata'][$alternateKey] ?? $this->extractBillingData($response, $key);
// Do not take any values from the billing address as a fallback for individual fields.
// If mandatory fields are missing from the shipping address, the complete shipping address is removed
return $response['addpaydata']['shipping_' . $key] ?? null;
}

private function getSalutationId(Context $context): string
Expand Down Expand Up @@ -145,4 +175,11 @@ private function getCountryIdByCode(string $code, Context $context): ?string

return $country->getId();
}

private function validateAddress(array $address, SalesChannelContext $salesChannelContext): ConstraintViolationList
{
$validation = $this->addressValidationFactory->create($salesChannelContext);

return $this->validator->getViolations($address, $validation);
}
}
72 changes: 72 additions & 0 deletions src/Components/Helper/ActivePaymentMethodsLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace PayonePayment\Components\Helper;

use Psr\Cache\CacheItemPoolInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class ActivePaymentMethodsLoader implements ActivePaymentMethodsLoaderInterface
{
public function __construct(
private readonly CacheItemPoolInterface $cachePool,
private readonly SalesChannelRepository $paymentMethodRepository,
private readonly EntityRepository $salesChannelRepository
) {
}

public function getActivePaymentMethodIds(SalesChannelContext $salesChannelContext): array
{
$cacheKey = $this->generateCacheKey($salesChannelContext->getSalesChannelId());

$cacheItem = $this->cachePool->getItem($cacheKey);

if ($cacheItem->get() === null) {
$cacheItem->set($this->collectActivePayonePaymentMethodIds($salesChannelContext));

$this->cachePool->save($cacheItem);
}

return $cacheItem->get();
}

public function clearCache(Context $context): void
{
$cacheKeys = [];

/** @var string[] $salesChannelIds */
$salesChannelIds = $this->salesChannelRepository->searchIds(new Criteria(), $context)->getIds();

foreach ($salesChannelIds as $salesChannelId) {
$cacheKeys[] = $this->generateCacheKey($salesChannelId);
}

if ($cacheKeys === []) {
return;
}

$this->cachePool->deleteItems($cacheKeys);
}

private function collectActivePayonePaymentMethodIds(SalesChannelContext $salesChannelContext): array
{
$criteria = new Criteria();

$criteria->addFilter(new ContainsFilter('handlerIdentifier', 'PayonePayment'));
$criteria->addFilter(new EqualsFilter('active', true));

return $this->paymentMethodRepository->searchIds($criteria, $salesChannelContext)->getIds();
}

private function generateCacheKey(string $salesChannelId): string
{
return 'payone_payment.active_payment_methods.' . $salesChannelId;
}
}
15 changes: 15 additions & 0 deletions src/Components/Helper/ActivePaymentMethodsLoaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace PayonePayment\Components\Helper;

use Shopware\Core\Framework\Context;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

interface ActivePaymentMethodsLoaderInterface
{
public function getActivePaymentMethodIds(SalesChannelContext $salesChannelContext): array;

public function clearCache(Context $context): void;
}
24 changes: 24 additions & 0 deletions src/Components/PaymentFilter/PaypalPaymentMethodFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace PayonePayment\Components\PaymentFilter;

use PayonePayment\Components\PaymentFilter\Exception\PaymentMethodNotAllowedException;
use PayonePayment\PaymentMethod\PayonePaypal;
use PayonePayment\PaymentMethod\PayonePaypalV2;
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\Checkout\Payment\PaymentMethodEntity;

class PaypalPaymentMethodFilter extends DefaultPaymentFilterService
{
protected function additionalChecks(PaymentMethodCollection $methodCollection, PaymentFilterContext $filterContext): void
{
$paypalV1 = $methodCollection->get(PayonePaypal::UUID);
$paypalV2 = $methodCollection->get(PayonePaypalV2::UUID);

if ($paypalV1 instanceof PaymentMethodEntity && $paypalV2 instanceof PaymentMethodEntity) {
throw new PaymentMethodNotAllowedException('PayPal: PayPal v1 is not allowed if v2 is active.');
}
}
}
4 changes: 4 additions & 0 deletions src/Configuration/ConfigurationPrefixes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface ConfigurationPrefixes
public const CONFIGURATION_PREFIX_DEBIT = 'debit';
public const CONFIGURATION_PREFIX_PAYPAL = 'paypal';
public const CONFIGURATION_PREFIX_PAYPAL_EXPRESS = 'paypalExpress';
public const CONFIGURATION_PREFIX_PAYPAL_V2 = 'paypalV2';
public const CONFIGURATION_PREFIX_PAYPAL_V2_EXPRESS = 'paypalV2Express';
public const CONFIGURATION_PREFIX_PAYOLUTION_INVOICING = 'payolutionInvoicing';
public const CONFIGURATION_PREFIX_PAYOLUTION_INSTALLMENT = 'payolutionInstallment';
public const CONFIGURATION_PREFIX_PAYOLUTION_DEBIT = 'payolutionDebit';
Expand Down Expand Up @@ -48,6 +50,8 @@ interface ConfigurationPrefixes
Handler\PayoneDebitPaymentHandler::class => self::CONFIGURATION_PREFIX_DEBIT,
Handler\PayonePaypalPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYPAL,
Handler\PayonePaypalExpressPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYPAL_EXPRESS,
Handler\PayonePaypalV2PaymentHandler::class => self::CONFIGURATION_PREFIX_PAYPAL_V2,
Handler\PayonePaypalV2ExpressPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYPAL_V2_EXPRESS,
Handler\PayonePayolutionInvoicingPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYOLUTION_INVOICING,
Handler\PayonePayolutionInstallmentPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYOLUTION_INSTALLMENT,
Handler\PayonePayolutionDebitPaymentHandler::class => self::CONFIGURATION_PREFIX_PAYOLUTION_DEBIT,
Expand Down
23 changes: 23 additions & 0 deletions src/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,29 @@ private function getPaymentParameters(string $paymentClass): array
'successurl' => 'https://www.payone.com',
];

case Handler\PayonePaypalV2ExpressPaymentHandler::class:
case Handler\PayonePaypalV2PaymentHandler::class:
return [
'request' => 'preauthorization',
'clearingtype' => 'wlt',
'wallettype' => 'PAL',
'amount' => 100,
'currency' => 'EUR',
'reference' => sprintf('%s%d', self::REFERENCE_PREFIX_TEST, random_int(1_000_000_000_000, 9_999_999_999_999)),
'firstname' => 'Test',
'lastname' => 'Test',
'country' => 'DE',
'successurl' => 'https://www.payone.com',
'errorurl' => 'https://www.payone.com',
'backurl' => 'https://www.payone.com',
'shipping_city' => 'Berlin',
'shipping_country' => 'DE',
'shipping_firstname' => 'Test',
'shipping_lastname' => 'Test',
'shipping_street' => 'Mustergasse 5',
'shipping_zip' => '10969',
];

case Handler\PayoneSofortBankingPaymentHandler::class:
return [
'request' => 'preauthorization',
Expand Down
Loading
Loading