diff --git a/app/code/Magento/BraintreeGraphQl/Model/BraintreeDataProvider.php b/app/code/Magento/BraintreeGraphQl/Model/BraintreeDataProvider.php
new file mode 100644
index 000000000000..23ca1d88e362
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/Model/BraintreeDataProvider.php
@@ -0,0 +1,49 @@
+config = $config;
+ $this->adapterFactory = $adapterFactory;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function resolve(
+ Field $field,
+ $context,
+ ResolveInfo $info,
+ array $value = null,
+ array $args = null
+ ) {
+ $storeId = (int)$context->getExtensionAttributes()->getStore()->getId();
+
+ if (!$this->config->isActive($storeId)) {
+ throw new GraphQlInputException(__('The Braintree payment method is not active.'));
+ }
+
+ $params = [];
+ $merchantAccountId = $this->config->getMerchantAccountId($storeId);
+ if (!empty($merchantAccountId)) {
+ $params[PaymentDataBuilder::MERCHANT_ACCOUNT_ID] = $merchantAccountId;
+ }
+
+ return $this->adapterFactory->create($storeId)->generate($params);
+ }
+}
diff --git a/app/code/Magento/BraintreeGraphQl/Plugin/SetVaultPaymentNonce.php b/app/code/Magento/BraintreeGraphQl/Plugin/SetVaultPaymentNonce.php
new file mode 100644
index 000000000000..1dea9992c630
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/Plugin/SetVaultPaymentNonce.php
@@ -0,0 +1,79 @@
+command = $command;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Set Braintree nonce from public hash
+ *
+ * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param array $paymentData
+ * @return array
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function beforeExecute(
+ \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject,
+ \Magento\Quote\Model\Quote $quote,
+ array $paymentData
+ ): array {
+ if ($paymentData['code'] !== ConfigProvider::CC_VAULT_CODE
+ || !isset($paymentData[ConfigProvider::CC_VAULT_CODE])
+ || !isset($paymentData[ConfigProvider::CC_VAULT_CODE]['public_hash'])
+ ) {
+ return [$quote, $paymentData];
+ }
+
+ $subject = [
+ 'public_hash' => $paymentData[ConfigProvider::CC_VAULT_CODE]['public_hash'],
+ 'customer_id' => $quote->getCustomerId(),
+ 'store_id' => $quote->getStoreId(),
+ ];
+
+ try {
+ $result = $this->command->execute($subject)->get();
+ $paymentData[ConfigProvider::CC_VAULT_CODE]['payment_method_nonce'] = $result['paymentMethodNonce'];
+ } catch (\Exception $e) {
+ $this->logger->critical($e);
+ throw new GraphQlInputException(__('Sorry, but something went wrong'));
+ }
+
+ return [$quote, $paymentData];
+ }
+}
diff --git a/app/code/Magento/BraintreeGraphQl/README.md b/app/code/Magento/BraintreeGraphQl/README.md
new file mode 100644
index 000000000000..f6740e4d250e
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/README.md
@@ -0,0 +1,4 @@
+# BraintreeGraphQl
+
+**BraintreeGraphQl** provides type and resolver for method additional
+information.
\ No newline at end of file
diff --git a/app/code/Magento/BraintreeGraphQl/composer.json b/app/code/Magento/BraintreeGraphQl/composer.json
new file mode 100644
index 000000000000..7790a4ac031d
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/composer.json
@@ -0,0 +1,28 @@
+{
+ "name": "magento/module-braintree-graph-ql",
+ "description": "N/A",
+ "type": "magento2-module",
+ "require": {
+ "php": "~7.1.3||~7.2.0||~7.3.0",
+ "magento/framework": "*",
+ "magento/module-braintree": "*",
+ "magento/module-store": "*",
+ "magento/module-quote": "*",
+ "magento/module-quote-graph-ql": "*"
+ },
+ "suggest": {
+ "magento/module-graph-ql": "*"
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Magento\\BraintreeGraphQl\\": ""
+ }
+ }
+}
diff --git a/app/code/Magento/BraintreeGraphQl/etc/graphql/di.xml b/app/code/Magento/BraintreeGraphQl/etc/graphql/di.xml
new file mode 100644
index 000000000000..a31066316377
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/etc/graphql/di.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ - Magento\BraintreeGraphQl\Model\BraintreeDataProvider
+ - Magento\BraintreeGraphQl\Model\BraintreeVaultDataProvider
+
+
+
+
+
+
+
diff --git a/app/code/Magento/BraintreeGraphQl/etc/module.xml b/app/code/Magento/BraintreeGraphQl/etc/module.xml
new file mode 100644
index 000000000000..dd12c0638b7b
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/etc/module.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls
new file mode 100644
index 000000000000..0492f8aaf989
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/etc/schema.graphqls
@@ -0,0 +1,22 @@
+# Copyright © Magento, Inc. All rights reserved.
+# See COPYING.txt for license details.
+
+type Mutation {
+ createBraintreeClientToken: String! @resolver(class: "\\Magento\\BraintreeGraphQl\\Model\\Resolver\\CreateBraintreeClientToken") @doc(description:"Creates Braintree Client Token for creating client-side nonce.")
+}
+
+input PaymentMethodInput {
+ braintree: BraintreeInput
+ braintree_cc_vault: BraintreeCcVaultInput
+}
+
+input BraintreeInput {
+ payment_method_nonce: String!
+ is_active_payment_token_enabler: Boolean!
+ device_data: String
+}
+
+input BraintreeCcVaultInput {
+ public_hash: String!
+ device_data: String
+}
diff --git a/app/code/Magento/BraintreeGraphQl/registration.php b/app/code/Magento/BraintreeGraphQl/registration.php
new file mode 100644
index 000000000000..37f7ef30864c
--- /dev/null
+++ b/app/code/Magento/BraintreeGraphQl/registration.php
@@ -0,0 +1,10 @@
+mockResponseDataProvider = $mockResponseDataProvider;
+ }
+
+ /**
+ * @param string $token
+ * @return \Braintree\Result\Successful|\Braintree\Result\Error
+ */
+ public function createNonce($token)
+ {
+ return $this->mockResponseDataProvider->generateMockNonceResponse($token);
+ }
+
+ /**
+ * @param array $attributes
+ * @return \Braintree\Result\Successful|\Braintree\Result\Error
+ */
+ public function sale(array $attributes)
+ {
+ return $this->mockResponseDataProvider->generateMockSaleResponse($attributes);
+ }
+
+ /**
+ * @param array $params
+ * @return string|\Braintree\Result\Error
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function generate(array $params = [])
+ {
+ return $this->mockResponseDataProvider->generateMockClientToken();
+ }
+}
diff --git a/dev/tests/api-functional/_files/Magento/TestModuleBraintree/Model/MockResponseDataProvider.php b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/Model/MockResponseDataProvider.php
new file mode 100644
index 000000000000..ab86109c6f91
--- /dev/null
+++ b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/Model/MockResponseDataProvider.php
@@ -0,0 +1,161 @@
+random = $random;
+ }
+
+ /**
+ * Create mock sale response for testing
+ *
+ * @param array $attributes
+ * @return \Braintree\Result\Error|\Braintree\Result\Successful
+ */
+ public function generateMockSaleResponse(array $attributes)
+ {
+ if (empty($attributes['paymentMethodNonce'])) {
+ return new \Braintree\Result\Error(
+ [
+ 'errors' => [
+ [
+ 'errorData' => [
+ 'code' => 2019,
+ 'message' => 'Your transaction has been declined.'
+ ]
+ ]
+ ],
+ 'transaction' => $this->createTransaction($attributes)->jsonSerialize(),
+ ]
+ );
+ }
+
+ $transaction = $this->createTransaction($attributes);
+
+ return new \Braintree\Result\Successful([$transaction]);
+ }
+
+ /**
+ * Create mock nonce response for testing
+ *
+ * @param string $token
+ * @return \Braintree\Instance
+ */
+ public function generateMockNonceResponse(string $token): \Braintree\Instance
+ {
+ $nonce = $this->createNonce($token);
+
+ return new \Braintree\Result\Successful($nonce, 'paymentMethodNonce');
+ }
+
+ /**
+ * Create mock client token
+ *
+ * @return string
+ */
+ public function generateMockClientToken(): string
+ {
+ return $this->random->getRandomString(32);
+ }
+
+ /**
+ * Create Braintree transaction from provided request attributes
+ *
+ * @param array $attributes
+ * @return \Braintree\Transaction
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function createTransaction(array $attributes): \Braintree\Transaction
+ {
+ $creditCardInfo = $this->generateCardDetails();
+ return \Braintree\Transaction::factory(
+ [
+ 'amount' => $attributes['amount'],
+ 'billing' => $attributes['billing'] ?? null,
+ 'creditCard' => $creditCardInfo,
+ 'cardDetails' => new \Braintree\Transaction\CreditCardDetails($creditCardInfo),
+ 'currencyIsoCode' => 'USD',
+ 'customer' => $attributes['customer'],
+ 'cvvResponseCode' => 'M',
+ 'id' => $this->random->getRandomString(8),
+ 'options' => $attributes['options'] ?? null,
+ 'shipping' => $attributes['shipping'] ?? null,
+ 'paymentMethodNonce' => $attributes['paymentMethodNonce'],
+ 'status' => 'authorized',
+ 'type' => 'sale',
+ ]
+ );
+ }
+
+ /**
+ * Generate fake Braintree card details
+ *
+ * @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function generateCardDetails(): array
+ {
+ return [
+ 'bin' => $this->random->getRandomString(6),
+ 'cardType' => 'Visa',
+ 'expirationMonth' => '12',
+ 'expirationYear' => '2020', //TODO: make dynamic
+ 'last4' => (string) random_int(1000, 9999),
+ 'token' => $this->random->getRandomString(6),
+ 'uniqueNumberIdentifier' => $this->random->getRandomString(32),
+ ];
+ }
+
+ /**
+ * Create fake Braintree nonce
+ *
+ * @param string $token
+ * @return \Braintree\PaymentMethodNonce
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function createNonce(string $token): \Braintree\PaymentMethodNonce
+ {
+ $lastFour = (string) random_int(1000, 9999);
+ $lastTwo = substr($lastFour, -2);
+ return \Braintree\PaymentMethodNonce::factory(
+ [
+ 'bin' => $token,
+ 'consumed' => false,
+ 'default' => true,
+ 'description' => 'ending in ' . $lastTwo,
+ 'details' => [
+ 'bin' => $this->random->getRandomString(6),
+ 'cardType' => 'Visa',
+ 'lastFour' => $lastFour,
+ 'lastTwo' => $lastTwo,
+ ],
+ 'hasSubscription' => false,
+ 'isLocked' => false,
+ 'nonce' => $this->random->getRandomString(36),
+ 'type' => 'CreditCard'
+ ]
+ );
+ }
+}
diff --git a/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/di.xml b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/di.xml
new file mode 100644
index 000000000000..db5c13684068
--- /dev/null
+++ b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/di.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/module.xml
new file mode 100644
index 000000000000..22df4e5fe7ea
--- /dev/null
+++ b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/etc/module.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/dev/tests/api-functional/_files/Magento/TestModuleBraintree/registration.php b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/registration.php
new file mode 100644
index 000000000000..c95e68f3f48f
--- /dev/null
+++ b/dev/tests/api-functional/_files/Magento/TestModuleBraintree/registration.php
@@ -0,0 +1,11 @@
+getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleBraintree') === null) {
+ ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleBraintree', __DIR__);
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php
new file mode 100644
index 000000000000..1564d00fa599
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/CreateBraintreeClientTokenTest.php
@@ -0,0 +1,53 @@
+graphQlMutation($this->getMutation());
+
+ self::assertArrayHasKey('createBraintreeClientToken', $response);
+ self::assertNotEmpty($response['createBraintreeClientToken']);
+ }
+
+ /**
+ * Test creating Braintree client token when method is disabled
+ *
+ * @expectedException \Exception
+ * @expectedExceptionMessage payment method is not active
+ */
+ public function testCreateBraintreeClientTokenNotActive()
+ {
+ $this->graphQlMutation($this->getMutation());
+ }
+
+ private function getMutation(): string
+ {
+ return <<getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class);
+ $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class);
+ $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+ $this->registry = Bootstrap::getObjectManager()->get(Registry::class);
+ $this->tokenCollectionFactory = Bootstrap::getObjectManager()->get(TokenCollectionFactory::class);
+ $this->tokenResource = Bootstrap::getObjectManager()->get(PaymentToken::class);
+ $this->getNonceCommand = Bootstrap::getObjectManager()->get(GetPaymentNonceCommand::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ */
+ public function testPlaceOrder()
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQuery($maskedQuoteId);
+ $setPaymentResponse = $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap());
+
+ $this->assertSetPaymentMethodResponse($setPaymentResponse, 'braintree');
+
+ $placeOrderQuery = $this->getPlaceOrderQuery($maskedQuoteId);
+ $placeOrderResponse = $this->graphQlMutation($placeOrderQuery, [], '', $this->getHeaderMap());
+
+ $this->assertPlaceOrderResponse($placeOrderResponse, $reservedOrderId);
+
+ $tokenQueryResult = $this->graphQlQuery($this->getPaymentTokenQuery(), [], '', $this->getHeaderMap());
+
+ self::assertArrayHasKey('customerPaymentTokens', $tokenQueryResult);
+ self::assertArrayHasKey('items', $tokenQueryResult['customerPaymentTokens']);
+ self::assertCount(0, $tokenQueryResult['customerPaymentTokens']['items']);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ */
+ public function testPlaceOrderSaveInVault()
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQuery($maskedQuoteId, true);
+ $setPaymentResponse = $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap());
+
+ $this->assertSetPaymentMethodResponse($setPaymentResponse, 'braintree');
+
+ $placeOrderQuery = $this->getPlaceOrderQuery($maskedQuoteId);
+ $placeOrderResponse = $this->graphQlMutation($placeOrderQuery, [], '', $this->getHeaderMap());
+
+ $this->assertPlaceOrderResponse($placeOrderResponse, $reservedOrderId);
+
+ $tokenQueryResult = $this->graphQlQuery($this->getPaymentTokenQuery(), [], '', $this->getHeaderMap());
+
+ self::assertArrayHasKey('customerPaymentTokens', $tokenQueryResult);
+ self::assertArrayHasKey('items', $tokenQueryResult['customerPaymentTokens']);
+ self::assertCount(1, $tokenQueryResult['customerPaymentTokens']['items']);
+ $token = current($tokenQueryResult['customerPaymentTokens']['items']);
+ self::assertArrayHasKey('payment_method_code', $token);
+ self::assertEquals('braintree', $token['payment_method_code']);
+ self::assertArrayHasKey('type', $token);
+ self::assertEquals('card', $token['type']);
+ self::assertArrayHasKey('details', $token);
+ self::assertArrayHasKey('public_hash', $token);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @magentoApiDataFixture Magento/GraphQl/Braintree/_files/token.php
+ */
+ public function testPlaceOrderWithVault()
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeVaultQuery(
+ $maskedQuoteId,
+ 'braintree_public_hash'
+ );
+ $setPaymentResponse = $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap());
+
+ $this->assertSetPaymentMethodResponse($setPaymentResponse, 'braintree_cc_vault');
+
+ $placeOrderQuery = $this->getPlaceOrderQuery($maskedQuoteId);
+ $placeOrderResponse = $this->graphQlMutation($placeOrderQuery, [], '', $this->getHeaderMap());
+
+ $this->assertPlaceOrderResponse($placeOrderResponse, $reservedOrderId);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @dataProvider dataProviderTestSetPaymentMethodInvalidInput
+ * @expectedException \Exception
+ * @param string $methodCode
+ */
+ public function testSetPaymentMethodInvalidInput(string $methodCode)
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidInput(
+ $maskedQuoteId,
+ $methodCode
+ );
+ $this->expectExceptionMessage("Required parameter \"$methodCode\" for \"payment_method\" is missing.");
+ $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap());
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree_cc_vault/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/Customer/_files/customer.php
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @dataProvider dataProviderTestSetPaymentMethodInvalidInput
+ * @expectedException \Exception
+ * @param string $methodCode
+ */
+ public function testSetPaymentMethodInvalidMethodInput(string $methodCode)
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidMethodInput(
+ $maskedQuoteId,
+ $methodCode
+ );
+ $this->expectExceptionMessage("for \"$methodCode\" is missing.");
+ $this->graphQlMutation($setPaymentQuery, [], '', $this->getHeaderMap());
+ }
+
+ public function dataProviderTestSetPaymentMethodInvalidInput(): array
+ {
+ return [
+ ['braintree'],
+ ['braintree_cc_vault'],
+ ];
+ }
+
+ private function assertPlaceOrderResponse(array $response, string $reservedOrderId): void
+ {
+ self::assertArrayHasKey('placeOrder', $response);
+ self::assertArrayHasKey('order', $response['placeOrder']);
+ self::assertArrayHasKey('order_id', $response['placeOrder']['order']);
+ self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']);
+ }
+
+ private function assertSetPaymentMethodResponse(array $response, string $methodCode): void
+ {
+ self::assertArrayHasKey('setPaymentMethodOnCart', $response);
+ self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']);
+ self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']);
+ self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']);
+ self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']);
+ }
+
+ /**
+ * @param string $maskedQuoteId
+ * @param bool $saveInVault
+ * @return string
+ */
+ private function getSetPaymentBraintreeQuery(string $maskedQuoteId, bool $saveInVault = false): string
+ {
+ $saveInVault = json_encode($saveInVault);
+ return <<customerTokenService->createCustomerAccessToken($username, $password);
+ $headerMap = ['Authorization' => 'Bearer ' . $customerToken];
+ return $headerMap;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function tearDown()
+ {
+ $this->registry->unregister('isSecureArea');
+ $this->registry->register('isSecureArea', true);
+
+ $orderCollection = $this->orderCollectionFactory->create();
+ foreach ($orderCollection as $order) {
+ $this->orderRepository->delete($order);
+ }
+
+ $tokenCollection = $this->tokenCollectionFactory->create();
+ foreach ($tokenCollection as $token) {
+ $this->tokenResource->delete($token);
+ }
+ $this->registry->unregister('isSecureArea');
+ $this->registry->register('isSecureArea', false);
+
+ parent::tearDown();
+ }
+}
diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php
new file mode 100644
index 000000000000..1d48c5253fe8
--- /dev/null
+++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Braintree/Guest/SetPaymentMethodTest.php
@@ -0,0 +1,286 @@
+getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class);
+ $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class);
+ $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class);
+ $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class);
+ $this->registry = Bootstrap::getObjectManager()->get(Registry::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @dataProvider dataProviderTestPlaceOrder
+ */
+ public function testPlaceOrder(string $nonce)
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQuery($maskedQuoteId, $nonce);
+ $setPaymentResponse = $this->graphQlMutation($setPaymentQuery);
+
+ $this->assertSetPaymentMethodResponse($setPaymentResponse, 'braintree');
+
+ $placeOrderQuery = $this->getPlaceOrderQuery($maskedQuoteId);
+ $placeOrderResponse = $this->graphQlMutation($placeOrderQuery);
+
+ $this->assertPlaceOrderResponse($placeOrderResponse, $reservedOrderId);
+ }
+
+ /**
+ * Data provider for testPlaceOrder
+ *
+ * @return array
+ */
+ public function dataProviderTestPlaceOrder(): array
+ {
+ return [
+ ['fake-valid-nonce'],
+ ['fake-apple-pay-visa-nonce'],
+ ];
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @expectedException \Exception
+ */
+ public function testSetPaymentMethodInvalidInput()
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidInput($maskedQuoteId);
+ $this->expectExceptionMessage("Required parameter \"braintree\" for \"payment_method\" is missing.");
+ $this->graphQlMutation($setPaymentQuery);
+ }
+
+ /**
+ * @magentoConfigFixture default_store carriers/flatrate/active 1
+ * @magentoConfigFixture default_store payment/braintree/active 1
+ * @magentoConfigFixture default_store payment/braintree/environment sandbox
+ * @magentoConfigFixture default_store payment/braintree/merchant_id def_merchant_id
+ * @magentoConfigFixture default_store payment/braintree/public_key def_public_key
+ * @magentoConfigFixture default_store payment/braintree/private_key def_private_key
+ * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php
+ * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php
+ * @expectedException \Exception
+ */
+ public function testSetPaymentMethodInvalidMethodInput()
+ {
+ $reservedOrderId = 'test_quote';
+ $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId);
+
+ $setPaymentQuery = $this->getSetPaymentBraintreeQueryInvalidMethodInput($maskedQuoteId);
+ $this->expectExceptionMessage("for \"braintree\" is missing.");
+ $this->graphQlMutation($setPaymentQuery);
+ }
+
+ private function assertPlaceOrderResponse(array $response, string $reservedOrderId): void
+ {
+ self::assertArrayHasKey('placeOrder', $response);
+ self::assertArrayHasKey('order', $response['placeOrder']);
+ self::assertArrayHasKey('order_id', $response['placeOrder']['order']);
+ self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_id']);
+ }
+
+ private function assertSetPaymentMethodResponse(array $response, string $methodCode): void
+ {
+ self::assertArrayHasKey('setPaymentMethodOnCart', $response);
+ self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']);
+ self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']);
+ self::assertArrayHasKey('code', $response['setPaymentMethodOnCart']['cart']['selected_payment_method']);
+ self::assertEquals($methodCode, $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']);
+ }
+
+ /**
+ * @param string $maskedQuoteId
+ * @return string
+ */
+ private function getSetPaymentBraintreeQuery(string $maskedQuoteId, string $nonce): string
+ {
+ return <<registry->unregister('isSecureArea');
+ $this->registry->register('isSecureArea', true);
+
+ $orderCollection = $this->orderCollectionFactory->create();
+ foreach ($orderCollection as $order) {
+ $this->orderRepository->delete($order);
+ }
+ $this->registry->unregister('isSecureArea');
+ $this->registry->register('isSecureArea', false);
+
+ parent::tearDown();
+ }
+}
diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Braintree/_files/token.php b/dev/tests/integration/testsuite/Magento/GraphQl/Braintree/_files/token.php
new file mode 100644
index 000000000000..8164e6d33e24
--- /dev/null
+++ b/dev/tests/integration/testsuite/Magento/GraphQl/Braintree/_files/token.php
@@ -0,0 +1,45 @@
+get(\Magento\Braintree\Model\Adapter\BraintreeAdapterFactory::class);
+$adapter = $adapterFactory->create();
+
+$result = $adapter->sale(
+ [
+ 'amount' => '0.01',
+ 'customer' => [
+ 'email' => 'customer@example.com',
+ 'firstName' => 'John',
+ 'lastName' => 'Smith'
+ ],
+ 'options' => ['storeInVaultOnSuccess' => true],
+ 'paymentMethodNonce' => 'fake-valid-nonce',
+ ]
+);
+
+$braintreeToken = $result->transaction->creditCardDetails->token;
+
+/** @var PaymentToken $token */
+$token = $objectManager->create(PaymentToken::class);
+
+$token->setGatewayToken($braintreeToken)
+ ->setPublicHash('braintree_public_hash')
+ ->setPaymentMethodCode('braintree_vault')
+ ->setType('card')
+ ->setExpiresAt(strtotime('+1 year'))
+ ->setIsVisible(true)
+ ->setIsActive(true)
+ ->setCustomerId(1);
+
+/** @var PaymentTokenRepository $tokenRepository */
+$tokenRepository = $objectManager->create(PaymentTokenRepository::class);
+$token = $tokenRepository->save($token);