diff --git a/src/Context/ActionSource.php b/src/Context/ActionSource.php index a99cfb2..4399949 100644 --- a/src/Context/ActionSource.php +++ b/src/Context/ActionSource.php @@ -10,14 +10,20 @@ class ActionSource { /** + * @deprecated 6.0.0 - $shopwareVersion should not be nullable + * * @param string $url The shop url * @param string $appVersion The installed App version * @param Collection $inAppPurchases The active in-app-purchases + * @param ?string $shopwareVersion The Shopware version provided by the header sw-version + * @param ?string $userLanguage The ISO-Code of the language used by the storefront customer or admin user */ public function __construct( public readonly string $url, public readonly string $appVersion, public readonly Collection $inAppPurchases, + public readonly ?string $shopwareVersion = null, + public readonly ?string $userLanguage = null, ) { } } diff --git a/src/Context/ContextResolver.php b/src/Context/ContextResolver.php index 0c3e58d..b5a6444 100644 --- a/src/Context/ContextResolver.php +++ b/src/Context/ContextResolver.php @@ -56,7 +56,7 @@ public function assembleWebhook(RequestInterface $request, ShopInterface $shop): return new WebhookAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), $body['data']['event'], $body['data']['payload'], new DateTimeImmutable('@' . $body['timestamp']) @@ -74,7 +74,7 @@ public function assembleActionButton(RequestInterface $request, ShopInterface $s return new ActionButtonAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), $body['data']['ids'], $body['data']['entity'], $body['data']['action'] @@ -119,7 +119,7 @@ public function assembleTaxProvider(RequestInterface $request, ShopInterface $sh return new TaxProviderAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new SalesChannelContext($body['context']), new Cart($body['cart']) ); @@ -136,7 +136,7 @@ public function assemblePaymentPay(RequestInterface $request, ShopInterface $sho return new PaymentPayAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Order($body['order']), new OrderTransaction($body['orderTransaction']), $body['returnUrl'] ?? null, @@ -156,7 +156,7 @@ public function assemblePaymentFinalize(RequestInterface $request, ShopInterface return new PaymentFinalizeAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new OrderTransaction($body['orderTransaction']), isset($body['recurring']) ? new RecurringData($body['recurring']) : null, // Support both Shopware 6.7 (requestData) and 6.6 (queryParameters) for backward compatibility @@ -175,7 +175,7 @@ public function assemblePaymentCapture(RequestInterface $request, ShopInterface return new PaymentCaptureAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Order($body['order']), new OrderTransaction($body['orderTransaction']), isset($body['recurring']) ? new RecurringData($body['recurring']) : null, @@ -194,7 +194,7 @@ public function assemblePaymentRecurringCapture(RequestInterface $request, ShopI return new PaymentRecurringAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Order($body['order']), new OrderTransaction($body['orderTransaction']), isset($body['recurring']) ? new RecurringData($body['recurring']) : null, @@ -212,7 +212,7 @@ public function assemblePaymentValidate(RequestInterface $request, ShopInterface return new PaymentValidateAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Cart($body['cart']), new SalesChannelContext($body['salesChannelContext']), $body['requestData'] ?? [] @@ -230,7 +230,7 @@ public function assemblePaymentRefund(RequestInterface $request, ShopInterface $ return new RefundAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Order($body['order']), new Refund($body['refund']), ); @@ -284,7 +284,7 @@ public function assembleCheckoutGatewayRequest(RequestInterface $request, ShopIn return new CheckoutGatewayAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Cart($body['cart']), new SalesChannelContext($body['salesChannelContext']), new Collection($this->arrayFlip($body['paymentMethods'])), @@ -303,7 +303,7 @@ public function assembleContextGatewayRequest(RequestInterface $request, ShopInt return new ContextGatewayAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Cart($body['cart']), new SalesChannelContext($body['salesChannelContext']), $body['data'], @@ -325,7 +325,7 @@ public function assembleInAppPurchasesFilterRequest(RequestInterface $request, S return new FilterAction( $shop, - $this->parseSource($body['source'], $shop), + $this->parseSource($body['source'], $shop, $request), new Collection($body['purchases']) ); } @@ -333,7 +333,7 @@ public function assembleInAppPurchasesFilterRequest(RequestInterface $request, S /** * @param array $source */ - private function parseSource(array $source, ShopInterface $shop): ActionSource + private function parseSource(array $source, ShopInterface $shop, RequestInterface $request): ActionSource { if (!isset($source['url'], $source['appVersion']) || !\is_string($source['url']) || !\is_string($source['appVersion'])) { throw new MalformedWebhookBodyException(); @@ -352,6 +352,8 @@ private function parseSource(array $source, ShopInterface $shop): ActionSource $source['url'], $source['appVersion'], $inAppPurchases ?? new Collection(), + $request->getHeaderLine('sw-version') ?: null, + $request->getHeaderLine('sw-user-language') ?: null, ); } diff --git a/tests/Context/ActionSourceTest.php b/tests/Context/ActionSourceTest.php index 27d20e4..74ef647 100644 --- a/tests/Context/ActionSourceTest.php +++ b/tests/Context/ActionSourceTest.php @@ -22,5 +22,21 @@ public function testConstructDefaults(): void static::assertSame($url, $source->url); static::assertSame($version, $source->appVersion); static::assertEquals(new Collection(), $source->inAppPurchases); + static::assertNull($source->shopwareVersion); + static::assertNull($source->userLanguage); + } + + public function testConstruct(): void + { + $url = 'https://example.com'; + $version = '1.0.0'; + + $source = new ActionSource($url, $version, new Collection(), '6.7.0.0', 'en-GB'); + + static::assertSame($url, $source->url); + static::assertSame($version, $source->appVersion); + static::assertEquals(new Collection(), $source->inAppPurchases); + static::assertSame('6.7.0.0', $source->shopwareVersion); + static::assertSame('en-GB', $source->userLanguage); } } diff --git a/tests/Context/ContextResolverTest.php b/tests/Context/ContextResolverTest.php index ee5ca59..ea749f7 100644 --- a/tests/Context/ContextResolverTest.php +++ b/tests/Context/ContextResolverTest.php @@ -1637,6 +1637,53 @@ public function testParseInAppPurchasesInvalid(): void $contextResolver->assembleWebhook($request, $this->getShop()); } + /** + * @param array $body + */ + #[DataProvider('sourceBodyProvider')] + public function testParseSourceReadsRequestHeaders(string $method, array $body): void + { + $request = new Request('POST', '/', [ + 'sw-version' => '6.7.0.0', + 'sw-user-language' => 'en-GB', + ], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + $action = $contextResolver->$method($request, $this->getShop()); + + static::assertSame('6.7.0.0', $action->source->shopwareVersion); + static::assertSame('en-GB', $action->source->userLanguage); + } + + /** + * @param array $body + */ + #[DataProvider('sourceBodyProvider')] + public function testParseSourceWithoutRequestHeaders(string $method, array $body): void + { + $request = new Request('POST', '/', [], \json_encode($body, \JSON_THROW_ON_ERROR)); + + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + $action = $contextResolver->$method($request, $this->getShop()); + + static::assertNull($action->source->shopwareVersion); + static::assertNull($action->source->userLanguage); + } + + public function testParseSourceWithEmptyRequestHeaders(): void + { + $request = new Request('POST', '/', [ + 'sw-version' => '', + 'sw-user-language' => '', + ], '{"source":{"url":"https://example.com","appVersion":"1.0.0"},"purchases":[]}'); + + $contextResolver = new ContextResolver($this->createMock(InAppPurchaseProvider::class)); + $action = $contextResolver->assembleInAppPurchasesFilterRequest($request, $this->getShop()); + + static::assertNull($action->source->shopwareVersion); + static::assertNull($action->source->userLanguage); + } + /** * @return iterable */ @@ -1677,6 +1724,70 @@ public static function methodsProvider(): iterable yield ['assembleInAppPurchasesFilterRequest']; } + /** + * A minimal valid body per assemble method that resolves a source. + * + * @return iterable}> + */ + public static function sourceBodyProvider(): iterable + { + $source = [ + 'source' => [ + 'url' => 'https://example.com', + 'appVersion' => '1.0.0', + ], + ]; + + yield 'assembleWebhook' => ['assembleWebhook', $source + [ + 'data' => ['event' => 'order.placed', 'payload' => []], + 'timestamp' => 123456789, + ]]; + yield 'assembleActionButton' => ['assembleActionButton', $source + [ + 'data' => ['ids' => ['123'], 'entity' => 'order', 'action' => 'open'], + ]]; + yield 'assembleTaxProvider' => ['assembleTaxProvider', $source + [ + 'context' => [], + 'cart' => [], + ]]; + yield 'assemblePaymentPay' => ['assemblePaymentPay', $source + [ + 'order' => [], + 'orderTransaction' => [], + ]]; + yield 'assemblePaymentFinalize' => ['assemblePaymentFinalize', $source + [ + 'orderTransaction' => [], + ]]; + yield 'assemblePaymentCapture' => ['assemblePaymentCapture', $source + [ + 'order' => [], + 'orderTransaction' => [], + ]]; + yield 'assemblePaymentRecurringCapture' => ['assemblePaymentRecurringCapture', $source + [ + 'order' => [], + 'orderTransaction' => [], + ]]; + yield 'assemblePaymentValidate' => ['assemblePaymentValidate', $source + [ + 'cart' => [], + 'salesChannelContext' => [], + ]]; + yield 'assemblePaymentRefund' => ['assemblePaymentRefund', $source + [ + 'order' => [], + 'refund' => [], + ]]; + yield 'assembleCheckoutGatewayRequest' => ['assembleCheckoutGatewayRequest', $source + [ + 'cart' => [], + 'salesChannelContext' => [], + 'paymentMethods' => [], + 'shippingMethods' => [], + ]]; + yield 'assembleContextGatewayRequest' => ['assembleContextGatewayRequest', $source + [ + 'cart' => [], + 'salesChannelContext' => [], + 'data' => [], + ]]; + yield 'assembleInAppPurchasesFilterRequest' => ['assembleInAppPurchasesFilterRequest', $source + [ + 'purchases' => [], + ]]; + } + /** * @return iterable */