Browse Source

chore: Scaffold for new Payments plugin

Relates to #1087
Michael Bromley 4 years ago
parent
commit
1037a97535

+ 3 - 0
packages/payment-plugin/.gitignore

@@ -0,0 +1,3 @@
+yarn-error.log
+/package
+e2e/__data__/*.sqlite

+ 30 - 0
packages/payment-plugin/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "@vendure/payments-plugin",
+  "version": "1.2.1",
+  "license": "MIT",
+  "main": "package/index.js",
+  "types": "package/index.d.ts",
+  "files": [
+    "package/**/*"
+  ],
+  "private": false,
+  "scripts": {
+    "watch": "tsc -p ./tsconfig.build.json --watch",
+    "build": "rimraf package && tsc -p ./tsconfig.build.json",
+    "lint": "tslint --fix --project ./",
+    "ci": "yarn build"
+  },
+  "homepage": "https://www.vendure.io/",
+  "funding": "https://github.com/sponsors/michaelbromley",
+  "publishConfig": {
+    "access": "public"
+  },
+  "devDependencies": {
+    "@types/braintree": "^2.22.15",
+    "@vendure/common": "^1.2.1",
+    "@vendure/core": "^1.2.1",
+    "braintree": "^3.0.0",
+    "rimraf": "^3.0.2",
+    "typescript": "4.3.5"
+  }
+}

+ 45 - 0
packages/payment-plugin/src/braintree/README.md

@@ -0,0 +1,45 @@
+# Braintree plugin
+
+This plugin enables payments to be processed by [Braintree](https://www.braintreepayments.com/), a popular payment provider.
+
+## Requirements
+
+1. You will need to create a Braintree sandbox account as outlined in https://developers.braintreepayments.com/start/overview.
+2. Then install `braintree` and `@types/braintree` from npm. This plugin was written with `v3.0.0` of the Braintree lib.
+
+## Setup
+1. Add the plugin to your VendureConfig `plugins` array.
+2. In the admin UI, fill in the `merchantId`, `publicKey` & `privateKey` from your Braintree sandbox account.
+
+## Usage
+
+The plugin is designed to work with the [Braintree drop-in UI](https://developers.braintreepayments.com/guides/drop-in/overview/javascript/v3).
+
+In your storefront, you'll have some code like this to submit the payment:
+
+```TypeScript
+async function submitPayment() {
+    const paymentResult = await this.dropin.requestPaymentMethod();
+    myGraphQlClient.mutation(gql`
+        mutation {
+            addPaymentToOrder(input: $input) {
+                id
+                state
+                payments {
+                    id
+                    amount
+                    errorMessage
+                    method
+                    state
+                    transactionId
+                    createdAt
+                }
+            }
+        }`, {
+        input: {
+            method: 'braintree',
+            metadata: paymentResult,
+        },
+    });
+}
+```

+ 70 - 0
packages/payment-plugin/src/braintree/braintree-common.ts

@@ -0,0 +1,70 @@
+import { BraintreeGateway, Environment, Transaction } from 'braintree';
+
+import { PaymentMethodArgsHash } from './types';
+
+export function getGateway(args: PaymentMethodArgsHash): BraintreeGateway {
+    return new BraintreeGateway({
+        environment: Environment.Sandbox,
+        merchantId: args.merchantId,
+        privateKey: args.privateKey,
+        publicKey: args.publicKey,
+    });
+}
+
+/**
+ * Returns a subset of the Transaction object of interest to the Administrator.
+ */
+export function extractMetadataFromTransaction(transaction: Transaction): { [key: string]: any } {
+    const metadata: { [key: string]: any } = {
+        status: transaction.status,
+        currencyIsoCode: transaction.currencyIsoCode,
+        merchantAccountId: transaction.merchantAccountId,
+        cvvCheck: decodeAvsCode(transaction.cvvResponseCode),
+        avsPostCodeCheck: decodeAvsCode(transaction.avsPostalCodeResponseCode),
+        avsStreetAddressCheck: decodeAvsCode(transaction.avsStreetAddressResponseCode),
+        processorAuthorizationCode: transaction.processorAuthorizationCode,
+        processorResponseText: transaction.processorResponseText,
+        paymentMethod: transaction.paymentInstrumentType,
+    };
+    if (transaction.creditCard && transaction.creditCard.cardType) {
+        metadata.cardData = {
+            cardType: transaction.creditCard.cardType,
+            last4: transaction.creditCard.last4,
+            expirationDate: transaction.creditCard.expirationDate,
+        };
+    }
+    if (transaction.paypalAccount && transaction.paypalAccount.authorizationId) {
+        metadata.paypalData = {
+            payerEmail: transaction.paypalAccount.payerEmail,
+            paymentId: transaction.paypalAccount.paymentId,
+            authorizationId: transaction.paypalAccount.authorizationId,
+            payerStatus: transaction.paypalAccount.payerStatus,
+            sellerProtectionStatus: transaction.paypalAccount.sellerProtectionStatus,
+            transactionFeeAmount: transaction.paypalAccount.transactionFeeAmount,
+        };
+    }
+    return metadata;
+}
+
+function decodeAvsCode(code: string): string {
+    switch (code) {
+        case 'I':
+            return 'Not Provided';
+        case 'M':
+            return 'Matched';
+        case 'N':
+            return 'Not Matched';
+        case 'U':
+            return 'Not Verified';
+        case 'S':
+            return 'Not Supported';
+        case 'E':
+            return 'AVS System Error';
+        case 'A':
+            return 'Not Applicable';
+        case 'B':
+            return 'Skipped';
+        default:
+            return 'Unknown';
+    }
+}

+ 79 - 0
packages/payment-plugin/src/braintree/braintree-payment-method.ts

@@ -0,0 +1,79 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { Logger, PaymentMethodHandler } from '@vendure/core';
+
+import { extractMetadataFromTransaction, getGateway } from './braintree-common';
+import { loggerCtx } from './constants';
+
+/**
+ * The handler for Braintree payments.
+ */
+export const braintreePaymentMethodHandler = new PaymentMethodHandler({
+    code: 'braintree',
+    description: [{ languageCode: LanguageCode.en, value: 'Braintree' }],
+    args: {
+        merchantId: { type: 'string' },
+        publicKey: { type: 'string' },
+        privateKey: { type: 'string' },
+    },
+
+    async createPayment(ctx, order, amount, args, metadata) {
+        const gateway = getGateway(args);
+        try {
+            const response = await gateway.transaction.sale({
+                amount: (amount / 100).toString(10),
+                orderId: order.code,
+                paymentMethodNonce: metadata.nonce,
+                options: {
+                    submitForSettlement: true,
+                },
+            });
+            if (!response.success) {
+                return {
+                    amount,
+                    state: 'Declined' as const,
+                    transactionId: response.transaction.id,
+                    errorMessage: response.message,
+                    metadata: extractMetadataFromTransaction(response.transaction),
+                };
+            }
+            return {
+                amount,
+                state: 'Settled' as const,
+                transactionId: response.transaction.id,
+                metadata: extractMetadataFromTransaction(response.transaction),
+            };
+        } catch (e) {
+            Logger.error(e, loggerCtx);
+            return {
+                amount: order.total,
+                state: 'Error' as const,
+                transactionId: '',
+                errorMessage: e.toString(),
+                metadata: e,
+            };
+        }
+    },
+
+    settlePayment() {
+        return {
+            success: true,
+        };
+    },
+
+    async createRefund(ctx, input, total, order, payment, args) {
+        const gateway = getGateway(args);
+        const response = await gateway.transaction.refund(payment.transactionId, (total / 100).toString(10));
+        if (!response.success) {
+            return {
+                state: 'Failed' as const,
+                transactionId: response.transaction.id,
+                metadata: response,
+            };
+        }
+        return {
+            state: 'Settled' as const,
+            transactionId: response.transaction.id,
+            metadata: response,
+        };
+    },
+});

+ 26 - 0
packages/payment-plugin/src/braintree/braintree-plugin.ts

@@ -0,0 +1,26 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { gql } from 'apollo-server-core';
+
+import { braintreePaymentMethodHandler } from './braintree-payment-method';
+import { BraintreeResolver } from './braintree.resolver';
+
+/**
+ * This plugin implements the Braintree (https://www.braintreepayments.com/) payment provider.
+ */
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [],
+    configuration: config => {
+        config.paymentOptions.paymentMethodHandlers.push(braintreePaymentMethodHandler);
+        return config;
+    },
+    shopApiExtensions: {
+        schema: gql`
+            extend type Query {
+                generateBraintreeClientToken(orderId: ID!): String!
+            }
+        `,
+        resolvers: [BraintreeResolver],
+    },
+})
+export class BraintreePlugin {}

+ 58 - 0
packages/payment-plugin/src/braintree/braintree.resolver.ts

@@ -0,0 +1,58 @@
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import {
+    Ctx,
+    ID,
+    InternalServerError,
+    Logger,
+    OrderService,
+    PaymentMethod,
+    RequestContext,
+    TransactionalConnection,
+} from '@vendure/core';
+
+import { getGateway } from './braintree-common';
+import { braintreePaymentMethodHandler } from './braintree-payment-method';
+import { loggerCtx } from './constants';
+import { PaymentMethodArgsHash } from './types';
+
+@Resolver()
+export class BraintreeResolver {
+    constructor(private connection: TransactionalConnection, private orderService: OrderService) {}
+
+    @Query()
+    async generateBraintreeClientToken(@Ctx() ctx: RequestContext, @Args() { orderId }: { orderId: ID }) {
+        const order = await this.orderService.findOne(ctx, orderId);
+        if (order && order.customer) {
+            const customerId = order.customer.id.toString();
+            const args = await this.getPaymentMethodArgs(ctx);
+            const gateway = getGateway(args);
+            try {
+                const result = await gateway.clientToken.generate({
+                    customerId,
+                });
+                return result.clientToken;
+            } catch (e) {
+                Logger.error(e);
+            }
+        } else {
+            throw new InternalServerError(`[${loggerCtx}] Could not find a Customer for the given Order`);
+        }
+    }
+
+    private async getPaymentMethodArgs(ctx: RequestContext): Promise<PaymentMethodArgsHash> {
+        const method = await this.connection.getRepository(ctx, PaymentMethod).findOne({
+            where: {
+                code: braintreePaymentMethodHandler.code,
+            },
+        });
+        if (!method) {
+            throw new InternalServerError(`[${loggerCtx}] Could not find Braintree PaymentMethod`);
+        }
+        return method.handler.args.reduce((hash, arg) => {
+            return {
+                ...hash,
+                [arg.name]: arg.value,
+            };
+        }, {} as PaymentMethodArgsHash);
+    }
+}

+ 1 - 0
packages/payment-plugin/src/braintree/constants.ts

@@ -0,0 +1 @@
+export const loggerCtx = 'BraintreePlugin';

+ 4 - 0
packages/payment-plugin/src/braintree/index.ts

@@ -0,0 +1,4 @@
+export * from './braintree-plugin';
+export * from './braintree-payment-method';
+export * from './braintree.resolver';
+export * from './braintree-common';

+ 5 - 0
packages/payment-plugin/src/braintree/types.ts

@@ -0,0 +1,5 @@
+import { ConfigArgValues } from '@vendure/core/dist/common/configurable-operation';
+
+import { braintreePaymentMethodHandler } from './braintree-payment-method';
+
+export type PaymentMethodArgsHash = ConfigArgValues<typeof braintreePaymentMethodHandler['args']>;

+ 4 - 0
packages/payment-plugin/src/index.ts

@@ -0,0 +1,4 @@
+/**
+ * This is a placeholder. Please import from one of the sub-packages, e.g `@vendure/payments-plugin/package/stripe`
+ */
+export const placeholder = 'Vendure Payments Plugin';

+ 1 - 0
packages/payment-plugin/src/paypal/README.md

@@ -0,0 +1 @@
+# Vendure Paypal integration

+ 1 - 0
packages/payment-plugin/src/stripe/README.md

@@ -0,0 +1 @@
+# Vendure Stripe integration

+ 9 - 0
packages/payment-plugin/tsconfig.build.json

@@ -0,0 +1,9 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./package"
+  },
+  "include": [
+    "./src/**/index.ts"
+  ]
+}

+ 10 - 0
packages/payment-plugin/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "declaration": true,
+    "removeComments": false,
+    "noLib": false,
+    "skipLibCheck": true,
+    "sourceMap": true
+  }
+}

+ 27 - 1
yarn.lock

@@ -1413,6 +1413,11 @@
     terminal-link "^2.1.1"
     yargs "^16.2.0"
 
+"@braintree/wrap-promise@2.1.0":
+  version "2.1.0"
+  resolved "https://registry.npmjs.org/@braintree/wrap-promise/-/wrap-promise-2.1.0.tgz#7e27ffc5dacd2d71533b0c42506eea8e7c2e50fa"
+  integrity sha512-UIrJB+AfKU0CCfbMoWrsGpd2D/hBpY/SGgFI6WRHPOwhaZ3g9rz1weiJ6eb6L9KgVyunT7s2tckcPkbHw+NzeA==
+
 "@cds/city@^1.1.0":
   version "1.1.0"
   resolved "https://registry.npmjs.org/@cds/city/-/city-1.1.0.tgz#5b7323750d3d64671ce2e3a804bcf260fbea1154"
@@ -3759,6 +3764,13 @@
     "@types/connect" "*"
     "@types/node" "*"
 
+"@types/braintree@^2.22.15":
+  version "2.22.15"
+  resolved "https://registry.npmjs.org/@types/braintree/-/braintree-2.22.15.tgz#b19a0e501f8ad9a3c245438c41408c31216d7257"
+  integrity sha512-e+C+3GRGUxaW6HbAXBrByCw2dK8qC9KZAOWg+H0cR2KWD3e2lyc2d2VIKnmzjsh5zT4yIhzewj2ga0kSCMt9sA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/component-emitter@^1.2.10":
   version "1.2.10"
   resolved "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
@@ -5874,6 +5886,15 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
   dependencies:
     fill-range "^7.0.1"
 
+braintree@^3.0.0:
+  version "3.7.0"
+  resolved "https://registry.npmjs.org/braintree/-/braintree-3.7.0.tgz#0f3ebcfab59362787788084f6c4a31365f4cacf1"
+  integrity sha512-UmMBTyRQeNeoMZQINOpbEc44fpzLNWG4tOjRdPG3EaarQeHiRJJ+E09/0ViOMQ8JVJ/OaK3swapPUt60lBeqew==
+  dependencies:
+    "@braintree/wrap-promise" "2.1.0"
+    dateformat "4.5.1"
+    xml2js "0.4.23"
+
 browser-process-hrtime@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
@@ -7447,6 +7468,11 @@ date-format@^3.0.0:
   resolved "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95"
   integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==
 
+dateformat@4.5.1:
+  version "4.5.1"
+  resolved "https://registry.npmjs.org/dateformat/-/dateformat-4.5.1.tgz#c20e7a9ca77d147906b6dc2261a8be0a5bd2173c"
+  integrity sha512-OD0TZ+B7yP7ZgpJf5K2DIbj3FZvFvxgFUuaqA/V5zTjAtAAXZ1E8bktHxmAGs4x5b7PflqA9LeQ84Og7wYtF7Q==
+
 dateformat@^3.0.0, dateformat@^3.0.3:
   version "3.0.3"
   resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
@@ -19191,7 +19217,7 @@ xml2js@0.4.19:
     sax ">=0.6.0"
     xmlbuilder "~9.0.1"
 
-xml2js@^0.4.17, xml2js@^0.4.23:
+xml2js@0.4.23, xml2js@^0.4.17, xml2js@^0.4.23:
   version "0.4.23"
   resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
   integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==