Skip to content

Commit

Permalink
feat: added customer vault token route
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianKe committed Feb 3, 2025
1 parent d0cb467 commit f1e3311
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 89 deletions.
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 <[email protected]>
* 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 <[email protected]>
* 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
4 changes: 2 additions & 2 deletions src/Resources/config/services/storefront.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,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 @@ -75,7 +76,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
3 changes: 3 additions & 0 deletions src/Storefront/Data/Service/VaultDataService.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public function buildData(SalesChannelContext $context): ?VaultData
return $struct;
}

/**
* @deprecated tag:v10.0.0 - Will be removed. Use `CustomerVaultTokenRoute::getVaultToken` instead
*/
public function getUserIdToken(SalesChannelContext $context): ?string
{
$customer = $context->getCustomer();
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 <[email protected]>
* 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::generateSalesChannelContext();

Check failure on line 45 in tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php

View workflow job for this annotation

GitHub Actions / phpstan (v6.6.0.0)

Call to an undefined static method Shopware\Core\Test\Generator::generateSalesChannelContext().
$salesChannelContext->assign(['customer' => null]);

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

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

public function testGetVaultTokenWithGuestCustomer(): void
{
$salesChannelContext = Generator::generateSalesChannelContext();

Check failure on line 55 in tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php

View workflow job for this annotation

GitHub Actions / phpstan (v6.6.0.0)

Call to an undefined static method Shopware\Core\Test\Generator::generateSalesChannelContext().
$salesChannelContext->getCustomer()?->setGuest(true);

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

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

public function testGetVaultToken(): void
{
$salesChannelContext = Generator::generateSalesChannelContext();

Check failure on line 65 in tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php

View workflow job for this annotation

GitHub Actions / phpstan (v6.6.0.0)

Call to an undefined static method Shopware\Core\Test\Generator::generateSalesChannelContext().
$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::generateSalesChannelContext();

Check failure on line 87 in tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php

View workflow job for this annotation

GitHub Actions / phpstan (v6.6.0.0)

Call to an undefined static method Shopware\Core\Test\Generator::generateSalesChannelContext().
$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

0 comments on commit f1e3311

Please sign in to comment.