Skip to content

Commit

Permalink
PAYOSWXP-158: Add PayPal v2 payment methods
Browse files Browse the repository at this point in the history
  • Loading branch information
momocode-de committed Oct 23, 2024
1 parent 38b2173 commit bcfb143
Show file tree
Hide file tree
Showing 50 changed files with 1,973 additions and 243 deletions.
113 changes: 72 additions & 41 deletions src/Components/GenericExpressCheckout/CustomerRegistrationUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PayonePayment\Components\GenericExpressCheckout;

use PayonePayment\Core\Utils\AddressCompare;
use RuntimeException;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\Framework\Context;
Expand All @@ -28,57 +29,72 @@ public function getCustomerDataBagFromGetCheckoutSessionResponse(array $response
{
$salutationId = $this->getSalutationId($context);

$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'),
]);

$isBillingAddressComplete = $this->hasAddressRequiredData($billingAddress);
$isShippingAddressComplete = $this->hasAddressRequiredData($shippingAddress);

if (!$isBillingAddressComplete && !$isShippingAddressComplete) {
throw new RuntimeException($this->translator->trans('PayonePayment.errorMessages.genericError'));
}

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'], /** @phpstan-ignore offsetAccess.notFound */
'lastName' => $billingAddress['lastName'], /** @phpstan-ignore offsetAccess.notFound */
'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
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 +108,16 @@ private function extractBillingData(array $response, string $key, string|null $a
}
}

if ($alternateKey === null
&& !\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
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 +157,23 @@ private function getCountryIdByCode(string $code, Context $context): ?string

return $country->getId();
}

private function hasAddressRequiredData(array $address): bool
{
$requiredFields = [
'firstName',
'lastName',
'city',
'street',
'countryId',
];

foreach ($requiredFields as $field) {
if (!isset($address[$field])) {
return false;
}
}

return true;
}
}
76 changes: 76 additions & 0 deletions src/Components/Helper/ActivePaymentMethodsLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?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());

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

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

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

return $activePaymentMethods->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;
}
6 changes: 6 additions & 0 deletions src/Configuration/ConfigurationPrefixes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ 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 +52,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
62 changes: 32 additions & 30 deletions src/Core/Utils/AddressCompare.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,33 @@

class AddressCompare
{
private const ADDRESS_FIELDS = [
'firstName',
'lastName',
'salutationId',
'company',
'street',
'additionalAddressLine1',
'additionalAddressLine2',
'zipcode',
'city',
'countryId',
'countryStateId',
];

public static function areOrderAddressesIdentical(OrderAddressEntity $entity1, OrderAddressEntity $entity2): bool
{
$fieldsToCompare = [
'firstName',
'lastName',
'salutationId',
'company',
'street',
'additionalAddressLine1',
'additionalAddressLine2',
'zipcode',
'city',
'countryId',
'countryStateId',
];

return self::areEntitiesIdentical($entity1, $entity2, $fieldsToCompare);
return self::areEntitiesIdentical($entity1, $entity2, self::ADDRESS_FIELDS);
}

public static function areCustomerAddressesIdentical(CustomerAddressEntity $entity1, CustomerAddressEntity $entity2): bool
{
$fieldsToCompare = [
'firstName',
'lastName',
'salutationId',
'company',
'street',
'additionalAddressLine1',
'additionalAddressLine2',
'zipcode',
'city',
'countryId',
'countryStateId',
];

return self::areEntitiesIdentical($entity1, $entity2, $fieldsToCompare);
return self::areEntitiesIdentical($entity1, $entity2, self::ADDRESS_FIELDS);
}

public static function areRawAddressesIdentical(array $address1, array $address2): bool
{
return self::areArraysIdentical($address1, $address2, self::ADDRESS_FIELDS);
}

private static function areEntitiesIdentical(Entity $entity1, Entity $entity2, array $fields): bool
Expand All @@ -58,4 +49,15 @@ private static function areEntitiesIdentical(Entity $entity1, Entity $entity2, a

return true;
}

private static function areArraysIdentical(array $array1, array $array2, array $fields): bool
{
foreach ($fields as $field) {
if (($array1[$field] ?? null) !== ($array2[$field] ?? null)) {
return false;
}
}

return true;
}
}
Loading

0 comments on commit bcfb143

Please sign in to comment.