Skip to content

Commit

Permalink
feat(payments-plugin): Add multi-currency support to braintree payments
Browse files Browse the repository at this point in the history
  • Loading branch information
kkerti committed Nov 26, 2024
1 parent 1167102 commit c859cd1
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 9 deletions.
132 changes: 132 additions & 0 deletions packages/payments-plugin/e2e/braintree-dev-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
import {
ChannelService,
configureDefaultOrderProcess,
DefaultLogger,
LanguageCode,
Logger,
LogLevel,
mergeConfig,
OrderService,
RequestContext,
} from '@vendure/core';
import {
createTestEnvironment,
registerInitializer,
SimpleGraphQLClient,
SqljsInitializer,
testConfig,
} from '@vendure/testing';
import gql from 'graphql-tag';
import path from 'path';

import { initialData } from '../../../e2e-common/e2e-initial-data';
import { BraintreePlugin } from '../src/braintree';
import { braintreePaymentMethodHandler } from '../src/braintree/braintree.handler';

/* eslint-disable */
import { CREATE_PAYMENT_METHOD } from './graphql/admin-queries';
import {
CreatePaymentMethodMutation,
CreatePaymentMethodMutationVariables,
} from './graphql/generated-admin-types';
import {
AddItemToOrderMutation,
AddItemToOrderMutationVariables,
AddPaymentToOrderMutation,
AddPaymentToOrderMutationVariables,
} from './graphql/generated-shop-types';
import { ADD_ITEM_TO_ORDER, ADD_PAYMENT } from './graphql/shop-queries';
import { GENERATE_BRAINTREE_CLIENT_TOKEN, proceedToArrangingPayment, setShipping } from './payment-helpers';
import braintree, { Environment, Test } from 'braintree';
import { BraintreeTestPlugin } from './fixtures/braintree-checkout-test.plugin';

export let clientToken: string;
export let exposedShopClient: SimpleGraphQLClient;

/**
* The actual starting of the dev server
*/
(async () => {
require('dotenv').config();

registerInitializer('sqljs', new SqljsInitializer(path.join(__dirname, '__data__')));
const config = mergeConfig(testConfig, {
authOptions: {
tokenMethod: ['bearer', 'cookie'],
cookieOptions: {
secret: 'cookie-secret',
},
},
plugins: [
...testConfig.plugins,
AdminUiPlugin.init({
route: 'admin',
port: 5001,
}),
BraintreePlugin.init({
storeCustomersInBraintree: false,
environment: Environment.Sandbox,
merchantAccountIds: {
USD: process.env.BRAINTREE_MERCHANT_ACCOUNT_ID_USD,
EUR: process.env.BRAINTREE_MERCHANT_ACCOUNT_ID_EUR,
},
}),
BraintreeTestPlugin,
],
logger: new DefaultLogger({ level: LogLevel.Debug }),
});
const { server, shopClient, adminClient } = createTestEnvironment(config as any);
exposedShopClient = shopClient;
await server.init({
initialData,
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 1,
});
// Create method
await adminClient.asSuperAdmin();
await adminClient.query<CreatePaymentMethodMutation, CreatePaymentMethodMutationVariables>(
CREATE_PAYMENT_METHOD,
{
input: {
code: 'braintree-payment-method',
enabled: true,
translations: [
{
name: 'Braintree',
description: 'This is a Braintree test payment method',
languageCode: LanguageCode.en,
},
],
handler: {
code: braintreePaymentMethodHandler.code,
arguments: [
{ name: 'privateKey', value: process.env.BRAINTREE_PRIVATE_KEY! },
{ name: 'publicKey', value: process.env.BRAINTREE_PUBLIC_KEY! },
{ name: 'merchantId', value: process.env.BRAINTREE_MERCHANT_ID! },
],
},
},
},
);
// Prepare order for payment
await shopClient.asUserWithCredentials('[email protected]', 'test');
await shopClient.query<AddItemToOrderMutation, AddItemToOrderMutationVariables>(ADD_ITEM_TO_ORDER, {
productVariantId: 'T_1',
quantity: 1,
});
const ctx = new RequestContext({
apiType: 'admin',
isAuthorized: true,
authorizedAsOwnerOnly: false,
channel: await server.app.get(ChannelService).getDefaultChannel(),
});
await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
description: 'Negative test surcharge',
listPrice: -20000,
});
await setShipping(shopClient);
const { generateBraintreeClientToken } = await shopClient.query(GENERATE_BRAINTREE_CLIENT_TOKEN);
clientToken = generateBraintreeClientToken;
Logger.info('http://localhost:3050/checkout', 'Braintree DevServer');
})();
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* eslint-disable */
import { Controller, Res, Get, Post, Body } from '@nestjs/common';
import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { Response } from 'express';

import { clientToken, exposedShopClient } from '../braintree-dev-server';
import { proceedToArrangingPayment } from '../payment-helpers';
import {
AddPaymentToOrderMutation,
AddPaymentToOrderMutationVariables,
} from '../graphql/generated-shop-types';
import { ADD_PAYMENT } from '../graphql/shop-queries';
/**
* This test controller returns the Braintree drop-in checkout page
* with the client secret generated by the dev-server
*/
@Controller()
export class BraintreeTestCheckoutController {
@Get('checkout')
async client(@Res() res: Response): Promise<void> {
res.send(`
<head>
<title>Checkout</title>
<script src="https://js.braintreegateway.com/web/dropin/1.33.3/js/dropin.min.js"></script>
</head>
<html>
<div id="dropin-container"></div>
<button id="submit-button">Purchase</button>
<div id="result"/>
<script>
var submitButton = document.querySelector('#submit-button');
braintree.dropin.create({
authorization: "${clientToken}",
container: '#dropin-container',
dataCollector: true,
paypal: {
flow: 'checkout',
amount: 100,
currency: 'GBP',
},
}, function (err, dropinInstance) {
submitButton.addEventListener('click', function () {
dropinInstance.requestPaymentMethod(async function (err, payload) {
sendPayloadToServer(payload)
});
});
if (dropinInstance.isPaymentMethodRequestable()) {
// This will be true if you generated the client token
// with a customer ID and there is a saved payment method
// available to tokenize with that customer.
submitButton.removeAttribute('disabled');
}
dropinInstance.on('paymentMethodRequestable', function (event) {
console.log(event.type); // The type of Payment Method, e.g 'CreditCard', 'PayPalAccount'.
console.log(event.paymentMethodIsSelected); // true if a customer has selected a payment method when paymentMethodRequestable fires
submitButton.removeAttribute('disabled');
});
dropinInstance.on('noPaymentMethodRequestable', function () {
submitButton.setAttribute('disabled', true);
});
});
async function sendPayloadToServer(payload) {
const response = await fetch('checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Credentials': 'include',
},
body: JSON.stringify(payload)
})
.then(res => res.json())
.catch(err => console.error(err))
document.querySelector('#result').innerHTML = JSON.stringify(response)
console.log(response)
}
</script>
</html>
`);
}
@Post('checkout')
async test(@Body() body: Request, @Res() res: Response): Promise<void> {
await proceedToArrangingPayment(exposedShopClient);
const { addPaymentToOrder } = await exposedShopClient.query<
AddPaymentToOrderMutation,
AddPaymentToOrderMutationVariables
>(ADD_PAYMENT, {
input: {
method: 'braintree-payment-method',
metadata: body,
},
});
console.log(addPaymentToOrder);

res.send(addPaymentToOrder);
}
}

/**
* Test plugin for serving the Stripe intent checkout page
*/
@VendurePlugin({
imports: [PluginCommonModule],
controllers: [BraintreeTestCheckoutController],
})
export class BraintreeTestPlugin {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Controller, Res, Get } from '@nestjs/common';
import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { Response } from 'express';

import { clientSecret } from './stripe-dev-server';
import { clientSecret } from '../stripe-dev-server';

/**
* This test controller returns the Stripe intent checkout page
Expand All @@ -18,8 +18,8 @@ export class StripeTestCheckoutController {
<title>Checkout</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<html>
<html>
<form id="payment-form">
<div id="payment-element">
<!-- Elements will create form elements here -->
Expand Down
6 changes: 6 additions & 0 deletions packages/payments-plugin/e2e/payment-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ export const CREATE_STRIPE_PAYMENT_INTENT = gql`
}
`;

export const GENERATE_BRAINTREE_CLIENT_TOKEN = gql`
query generateBraintreeClientToken {
generateBraintreeClientToken
}
`;

export const GET_MOLLIE_PAYMENT_METHODS = gql`
query molliePaymentMethods($input: MolliePaymentMethodsInput!) {
molliePaymentMethods(input: $input) {
Expand Down
2 changes: 1 addition & 1 deletion packages/payments-plugin/e2e/stripe-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from './graphql/generated-shop-types';
import { ADD_ITEM_TO_ORDER } from './graphql/shop-queries';
import { CREATE_STRIPE_PAYMENT_INTENT, setShipping } from './payment-helpers';
import { StripeCheckoutTestPlugin } from './stripe-checkout-test.plugin';
import { StripeCheckoutTestPlugin } from './fixtures/stripe-checkout-test.plugin';

export let clientSecret: string;

Expand Down
21 changes: 20 additions & 1 deletion packages/payments-plugin/src/braintree/braintree-common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CurrencyCode } from '@vendure/core';
import { BraintreeGateway, Environment, Transaction } from 'braintree';

import { BraintreePluginOptions, PaymentMethodArgsHash } from './types';
import { BraintreeMerchantAccountIds, BraintreePluginOptions, PaymentMethodArgsHash } from './types';

export function getGateway(args: PaymentMethodArgsHash, options: BraintreePluginOptions): BraintreeGateway {
return new BraintreeGateway({
Expand Down Expand Up @@ -74,3 +75,21 @@ function decodeAvsCode(code: string): string {
return 'Unknown';
}
}

/**
* @description
* Looks up a single mmerchantAccountId from `merchantAccountIds` passed through the plugin options.
* Example: `{NOK: BRAINTREE_MERCHANT_ACCOUNT_ID_NOK}` for Norway.
* Merchant Account IDs have to be setup in the Braintree dashboard,
* see: https://developer.paypal.com/braintree/articles/control-panel/important-gateway-credentials#merchant-account-id
*/
export function lookupMerchantAccountIdByCurrency(
merchantAccountIds: BraintreeMerchantAccountIds | undefined,
currencyCode: CurrencyCode,
): string | undefined {
if (!merchantAccountIds || !currencyCode) {
return undefined;
}
const merchantAccountIdForCurrency = merchantAccountIds[currencyCode];
return merchantAccountIdForCurrency;
}
8 changes: 7 additions & 1 deletion packages/payments-plugin/src/braintree/braintree.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@vendure/core';
import { BraintreeGateway } from 'braintree';

import { defaultExtractMetadataFn, getGateway } from './braintree-common';
import { defaultExtractMetadataFn, getGateway, lookupMerchantAccountIdByCurrency } from './braintree-common';
import { BRAINTREE_PLUGIN_OPTIONS, loggerCtx } from './constants';
import { BraintreePluginOptions } from './types';

Expand Down Expand Up @@ -95,11 +95,17 @@ async function processPayment(
customerId: string | undefined,
pluginOptions: BraintreePluginOptions,
) {
const merchantAccountId = lookupMerchantAccountIdByCurrency(
options.merchantAccountIds,
order.currencyCode,
);

const response = await gateway.transaction.sale({
customerId,
amount: (amount / 100).toString(10),
orderId: order.code,
paymentMethodNonce,
merchantAccountId,
options: {
submitForSettlement: true,
storeInVaultOnSuccess: !!customerId,
Expand Down
1 change: 1 addition & 0 deletions packages/payments-plugin/src/braintree/braintree.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ import { BraintreePluginOptions } from './types';
* @docsCategory core plugins/PaymentsPlugin
* @docsPage BraintreePlugin
*/

@VendurePlugin({
imports: [PluginCommonModule],
providers: [
Expand Down
Loading

0 comments on commit c859cd1

Please sign in to comment.