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

feat: added customer vault token route #90

Merged
merged 2 commits into from
Feb 11, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 9.6.6
- PPI-1044 - Improved compatibility of Vaulting with Store API usage and Headless setups

# 9.6.5
- PPI-1025 - Improves the performance of the installment banner in the Storefront
- PPI-1043 - Fixes an issue, where a payment method is toggled twice in the Administration
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG_de-DE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 9.6.6
- PPI-1044 - Verbesserte Kompatibilität von Vaulting mit der Store-API und Headless

# 9.6.5
- PPI-1025 - Verbessert die Performance des Ratenzahlungsbanners in der Storefront
- PPI-1043 - Behebt ein Problem, bei dem eine Zahlungsmethode doppelt umgeschalten wurde
Expand Down
34 changes: 34 additions & 0 deletions src/Checkout/Exception/MissingCustomerVaultTokenException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Swag\PayPal\Checkout\Exception;

use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\ShopwareHttpException;
use Symfony\Component\HttpFoundation\Response;

#[Package('checkout')]
class MissingCustomerVaultTokenException extends ShopwareHttpException
{
public function __construct(string $customerId)
{
parent::__construct(
'Missing vault token for customer "{{ customerId }}"',
['customerId' => $customerId]
);
}

public function getStatusCode(): int
{
return Response::HTTP_BAD_REQUEST;
}

public function getErrorCode(): string
{
return 'SWAG_PAYPAL__MISSING_CUSTOMER_VAULT_TOKEN';
}
}
74 changes: 74 additions & 0 deletions src/Checkout/SalesChannel/CustomerVaultTokenRoute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Swag\PayPal\Checkout\SalesChannel;

use OpenApi\Attributes as OA;
use Shopware\Core\Checkout\Customer\CustomerException;
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\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Swag\PayPal\Checkout\Exception\MissingCustomerVaultTokenException;
use Swag\PayPal\Checkout\TokenResponse;
use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenEntity;
use Swag\PayPal\RestApi\V1\Resource\TokenResourceInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Package('checkout')]
#[Route(defaults: ['_routeScope' => ['store-api']])]
class CustomerVaultTokenRoute
{
/**
* @internal
*/
public function __construct(
private EntityRepository $vaultRepository,
private TokenResourceInterface $tokenResource,
) {
}

#[OA\Get(
path: '/paypal/vault-token',
operationId: 'getPayPalCustomerVaultToken',
description: 'Tries to get the customer vault token',
tags: ['Store API', 'PayPal'],
responses: [new OA\Response(
response: Response::HTTP_OK,
description: 'The customer vault token',
content: new OA\JsonContent(properties: [new OA\Property(
property: 'token',
type: 'string'
)])
)],
)]
#[Route(path: '/store-api/paypal/vault-token', name: 'store-api.paypal.vault.token', defaults: ['_loginRequired' => true, '_loginRequiredAllowGuest' => false], methods: ['GET'])]
public function getVaultToken(SalesChannelContext $context): TokenResponse
{
$customer = $context->getCustomer();
if (!$customer || $customer->getGuest()) {
throw CustomerException::customerNotLoggedIn();
}

$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('mainMapping.customerId', $customer->getId()));
$criteria->addFilter(new EqualsFilter('mainMapping.paymentMethodId', $context->getPaymentMethod()->getId()));

/** @var VaultTokenEntity|null $vault */
$vault = $this->vaultRepository->search($criteria, $context->getContext())->first();

$token = $this->tokenResource->getUserIdToken($context->getSalesChannelId(), $vault?->getTokenCustomer())->getIdToken();

if ($token === null) {
throw new MissingCustomerVaultTokenException($customer->getId());
}

return new TokenResponse($token);
}
}
27 changes: 27 additions & 0 deletions src/Resources/Schema/StoreApi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,33 @@
}
}
},
"/paypal/vault-token": {
"get": {
"tags": [
"Store API",
"PayPal"
],
"description": "Tries to get the customer vault token",
"operationId": "getPayPalCustomerVaultToken",
"responses": {
"200": {
"description": "The customer vault token",
"content": {
"application/json": {
"schema": {
"properties": {
"token": {
"type": "string"
}
},
"type": "object"
}
}
}
}
}
}
},
"/paypal/payment-method-eligibility": {
"post": {
"tags": [
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/services/checkout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<argument type="service" id="Shopware\Core\Checkout\Payment\Cart\PaymentTransactionStructFactory"/>
</service>

<service id="Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute" public="true">
<argument type="service" id="swag_paypal_vault_token.repository"/>
<argument type="service" id="Swag\PayPal\RestApi\V1\Resource\TokenResource" />
</service>

<service id="Swag\PayPal\Checkout\SalesChannel\FilteredPaymentMethodRoute"
decorates="Shopware\Core\Checkout\Payment\SalesChannel\PaymentMethodRoute"
decoration-priority="-1500"
Expand Down
5 changes: 2 additions & 3 deletions src/Resources/config/services/storefront.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
<service id="Swag\PayPal\Storefront\Data\Service\VaultDataService">
<argument type="service" id="swag_paypal_vault_token.repository"/>
<argument type="service" id="Swag\PayPal\Util\Lifecycle\Method\PaymentMethodDataRegistry"/>
<argument type="service" id="Swag\PayPal\RestApi\V1\Resource\TokenResource"/>
</service>

<service id="Swag\PayPal\Storefront\Data\Service\AbstractCheckoutDataService" abstract="true">
Expand All @@ -77,10 +76,10 @@
<service id="Swag\PayPal\Storefront\Data\Service\PayLaterCheckoutDataService" public="true" parent="Swag\PayPal\Storefront\Data\Service\AbstractCheckoutDataService" />
<service id="Swag\PayPal\Storefront\Data\Service\SEPACheckoutDataService" public="true" parent="Swag\PayPal\Storefront\Data\Service\AbstractCheckoutDataService" />
<service id="Swag\PayPal\Storefront\Data\Service\SPBCheckoutDataService" public="true" parent="Swag\PayPal\Storefront\Data\Service\AbstractCheckoutDataService">
<argument type="service" id="Swag\PayPal\Storefront\Data\Service\VaultDataService"/>
<argument type="service" id="Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute"/>
</service>
<service id="Swag\PayPal\Storefront\Data\Service\VenmoCheckoutDataService" public="true" parent="Swag\PayPal\Storefront\Data\Service\AbstractCheckoutDataService">
<argument type="service" id="Swag\PayPal\Storefront\Data\Service\VaultDataService"/>
<argument type="service" id="Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute"/>
</service>

<service id="Swag\PayPal\Storefront\Data\CheckoutDataSubscriber">
Expand Down
5 changes: 3 additions & 2 deletions src/Storefront/Data/Service/SPBCheckoutDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Swag\PayPal\Checkout\ExpressCheckout\SalesChannel\ExpressPrepareCheckoutRoute;
use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\Checkout\SPBCheckout\SPBCheckoutButtonData;
use Swag\PayPal\Setting\Service\CredentialsUtilInterface;
use Swag\PayPal\Setting\Settings;
Expand All @@ -37,7 +38,7 @@ public function __construct(
RouterInterface $router,
SystemConfigService $systemConfigService,
CredentialsUtilInterface $credentialsUtil,
private readonly VaultDataService $vaultDataService,
private readonly CustomerVaultTokenRoute $customerVaultTokenRoute,
) {
parent::__construct($paymentMethodDataRegistry, $localeCodeProvider, $router, $systemConfigService, $credentialsUtil);
}
Expand Down Expand Up @@ -73,7 +74,7 @@ public function buildCheckoutData(
'useAlternativePaymentMethods' => $this->systemConfigService->getBool(Settings::SPB_ALTERNATIVE_PAYMENT_METHODS_ENABLED, $salesChannelId),
'disabledAlternativePaymentMethods' => $this->getDisabledAlternativePaymentMethods($price, $currency->getIsoCode()),
'showPayLater' => $this->systemConfigService->getBool(Settings::SPB_SHOW_PAY_LATER, $salesChannelId),
'userIdToken' => $this->vaultDataService->getUserIdToken($context),
'userIdToken' => $this->customerVaultTokenRoute->getVaultToken($context)->getToken(),
]));
}

Expand Down
16 changes: 0 additions & 16 deletions src/Storefront/Data/Service/VaultDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ class VaultDataService
public function __construct(
private readonly EntityRepository $vaultRepository,
private readonly PaymentMethodDataRegistry $paymentMethodDataRegistry,
private readonly TokenResourceInterface $tokenResource,
) {
}

Expand Down Expand Up @@ -61,21 +60,6 @@ public function buildData(SalesChannelContext $context): ?VaultData
return $struct;
}

public function getUserIdToken(SalesChannelContext $context): ?string
{
$customer = $context->getCustomer();
if ($customer === null || $customer->getGuest() === true) {
return null;
}

$vault = $this->fetchVaultData($customer, $context);
if ($vault !== null) {
return $this->tokenResource->getUserIdToken($context->getSalesChannelId(), $vault->getTokenCustomer())->getIdToken();
}

return $this->tokenResource->getUserIdToken($context->getSalesChannelId())->getIdToken();
}

private function fetchVaultData(CustomerEntity $customer, SalesChannelContext $context): ?VaultTokenEntity
{
$criteria = new Criteria();
Expand Down
5 changes: 3 additions & 2 deletions src/Storefront/Data/Service/VenmoCheckoutDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\Setting\Service\CredentialsUtilInterface;
use Swag\PayPal\Storefront\Data\Struct\VenmoCheckoutData;
use Swag\PayPal\Util\Lifecycle\Method\PaymentMethodDataRegistry;
Expand All @@ -31,7 +32,7 @@ public function __construct(
RouterInterface $router,
SystemConfigService $systemConfigService,
CredentialsUtilInterface $credentialsUtil,
private readonly VaultDataService $vaultDataService,
private readonly CustomerVaultTokenRoute $customerVaultTokenRoute,
) {
parent::__construct($paymentMethodDataRegistry, $localeCodeProvider, $router, $systemConfigService, $credentialsUtil);
}
Expand All @@ -41,7 +42,7 @@ public function buildCheckoutData(SalesChannelContext $context, ?Cart $cart = nu
$data = $this->getBaseData($context, $order);

return (new VenmoCheckoutData())->assign(\array_merge($data, [
'userIdToken' => $this->vaultDataService->getUserIdToken($context),
'userIdToken' => $this->customerVaultTokenRoute->getVaultToken($context)->getToken(),
]));
}

Expand Down
103 changes: 103 additions & 0 deletions tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php declare(strict_types=1);
/*
* (c) shopware AG <info@shopware.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Swag\PayPal\Test\Checkout\SalesChannel;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Checkout\Customer\CustomerException;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Test\Generator;
use Swag\PayPal\Checkout\Exception\MissingCustomerVaultTokenException;
use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenEntity;
use Swag\PayPal\RestApi\V1\Api\Token;
use Swag\PayPal\RestApi\V1\Resource\TokenResourceInterface;

/**
* @internal
*/
#[Package('checkout')]
class CustomerVaultTokenRouteTest extends TestCase
{
private EntityRepository&MockObject $repository;

private TokenResourceInterface&MockObject $tokenResource;

private CustomerVaultTokenRoute $route;

protected function setUp(): void
{
$this->repository = $this->createMock(EntityRepository::class);
$this->tokenResource = $this->createMock(TokenResourceInterface::class);

$this->route = new CustomerVaultTokenRoute($this->repository, $this->tokenResource);
}

public function testGetVaultTokenWithoutCustomer(): void
{
$salesChannelContext = Generator::createSalesChannelContext();
$salesChannelContext->assign(['customer' => null]);

$this->expectException(CustomerException::class);

$this->route->getVaultToken($salesChannelContext);
}

public function testGetVaultTokenWithGuestCustomer(): void
{
$salesChannelContext = Generator::createSalesChannelContext();
$salesChannelContext->getCustomer()?->setGuest(true);

$this->expectException(CustomerException::class);

$this->route->getVaultToken($salesChannelContext);
}

public function testGetVaultToken(): void
{
$salesChannelContext = Generator::createSalesChannelContext();
$salesChannelContext->getCustomer()?->setGuest(false);

$entitySearchResult = $this->createMock(EntitySearchResult::class);
$this->repository->expects(static::once())->method('search')->willReturn($entitySearchResult);
$entitySearchResult->expects(static::once())->method('first')->willReturn(new VaultTokenEntity());

$token = new Token();
$token->assign(['idToken' => 'dummy-token', 'expiresIn' => 45000]);

$this->tokenResource->expects(static::once())->method('getUserIdToken')->willReturn($token);

$response = $this->route->getVaultToken($salesChannelContext);

static::assertSame(
$token->getIdToken(),
$response->getToken()
);
}

public function testVaultTokenIsNull(): void
{
$salesChannelContext = Generator::createSalesChannelContext();
$salesChannelContext->getCustomer()?->setGuest(false);

$entitySearchResult = $this->createMock(EntitySearchResult::class);
$this->repository->expects(static::once())->method('search')->willReturn($entitySearchResult);
$entitySearchResult->expects(static::once())->method('first')->willReturn(new VaultTokenEntity());

$token = new Token();
$token->assign(['idToken' => null, 'expiresIn' => 45000]);

$this->tokenResource->expects(static::once())->method('getUserIdToken')->willReturn($token);

$this->expectException(MissingCustomerVaultTokenException::class);

$this->route->getVaultToken($salesChannelContext);
}
}
Loading
Loading