Преглед изворни кода

feat(docs): Improve docs on payment integrations

Michael Bromley пре 5 година
родитељ
комит
50d2b3fff4

+ 0 - 49
docs/content/docs/developer-guide/payment-integrations.md

@@ -1,49 +0,0 @@
----
-title: "Payment Integrations"
-showtoc: true
----
-
-# Payment Integrations
-
-Vendure can support many kinds of payment workflows, such as authorizing and capturing payment in a single step upon checkout or authorizing on checkout and then capturing on fulfillment. 
-
-{{< alert "primary" >}}
-  For a complete working example of a real payment integration, see the [real-world-vendure Braintree plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/braintree)
-{{< /alert >}}
-
-## Creating an integration
-
-Payment integrations are created by creating a new [PaymentMethodHandler]({{< relref "payment-method-handler" >}}) and passing that handler into the [`paymentOptions.paymentMethodHandlers`]({{< relref "payment-options" >}}) array in the VendureConfig.
-
-```TypeScript
-import { PaymentMethodHandler, VendureConfig } from '@vendure/core';
-
-const myPaymentIntegration = new PaymentMethodHandler({
-    code: 'my-payment-method',
-    // ... 
-    // configuration of the handler (see PaymentMethodConfigOptions docs)
-});
-
-export const config: VendureConfig = {
-    // ...
-    paymentOptions: {
-        paymentMethodHandlers: [myPaymentIntegration],
-    },
-};
-```
-
-For a more complete example of a payment integration, see the [PaymentMethodHandler]({{< relref "payment-method-handler" >}}) documentation.
-
-## Using an integration
-
-In your storefront application, this payment method can then be used when executing the [`addPaymentToOrder` mutation]({{< relref "/docs/graphql-api/shop/mutations#addpaymenttoorder" >}}), as the "method" field of the [`PaymentInput` object]({{< relref "/docs/graphql-api/shop/input-types#paymentinput" >}}).
-
-```SDL
-mutation {
-    addPaymentToOrder(input: { 
-        method: "my-payment-method",
-        metadata: { id: "<some id from the payment provider>" }) {
-        ...Order
-    }
-}
-```

+ 127 - 0
docs/content/docs/developer-guide/payment-integrations/index.md

@@ -0,0 +1,127 @@
+---
+title: "Payment Integrations"
+showtoc: true
+---
+
+# Payment Integrations
+
+Vendure can support many kinds of payment workflows, such as authorizing and capturing payment in a single step upon checkout or authorizing on checkout and then capturing on fulfillment. 
+
+{{< alert "primary" >}}
+  For a complete working example of a real payment integration, see the [real-world-vendure Braintree plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/braintree)
+{{< /alert >}}
+
+## Authorization & Settlement
+
+Typically, there are 2 parts to an online payment: **authorization** and **settlement**:
+
+* **Authorization** is the process by which the customer's bank is contacted to check whether the transaction is allowed. At this stage, no funds are removed from the customer's account.
+* **Settlement** (also known as "capture") is the process by which the funds are transferred from the customer's account to the merchant.
+
+Some merchants do both of these steps at once, when the customer checks out of the store. Others do the authorize step at checkout, and only do the settlement at some later point, e.g. upon shipping the goods to the customer.
+
+This two-step workflow can also be applied to other non-card forms of payment: e.g. if providing a "payment on delivery" option, the authorization step would occur on checkout, and the settlement step would be triggered upon delivery, either manually by an administrator of via an app integration with the Admin API.
+
+## Creating an integration
+
+Payment integrations are created by defining a new [PaymentMethodHandler]({{< relref "payment-method-handler" >}}) and passing that handler into the [`paymentOptions.paymentMethodHandlers`]({{< relref "payment-options" >}}) array in the VendureConfig.
+
+```TypeScript
+import { PaymentMethodHandler, VendureConfig, CreatePaymentResult, SettlePaymentResult } from '@vendure/core';
+import { sdk } from 'payment-provider-sdk';
+
+/**
+ * This is a handler which integrates Vendure with an imaginary
+ * payment provider, who provide a Node SDK which we use to 
+ * interact with their APIs.
+ */
+const myPaymentIntegration = new PaymentMethodHandler({
+  code: 'my-payment-method',
+  description: [{
+    languageCode: LanguageCode.en,
+    value: 'My Payment Provider',
+  }],
+  args: {
+    apiKey: { type: 'string' },
+  },
+
+  /** This is called when the `addPaymentToOrder` mutation is executed */
+  createPayment: async (order, args, metadata): Promise<CreatePaymentResult> => {
+    try {
+      const result = await sdk.charges.create({
+        apiKey: args.apiKey,
+        amount: order.total,
+        source: metadata.token,
+      });
+      return {
+        amount: order.total,
+        state: 'Authorized' as const,
+        transactionId: result.id.toString(),
+        metadata: result.outcome,
+      };
+    } catch (err) {
+      return {
+        amount: order.total,
+        state: 'Declined' as const,
+        metadata: {
+          errorMessage: err.message,
+        },
+      };
+    }
+  },
+
+  /** This is called when the `settlePayment` mutation is executed */
+  settlePayment: async (order, payment, args): Promise<SettlePaymentResult> => {
+    try {
+      const result = await sdk.charges.capture({ 
+        apiKey: args.apiKey,
+        id: payment.transactionId,
+      });
+      return { success: true };   
+    } catch (err) {
+      return {
+        success: false,
+        errorMessage: err.message,
+      }
+    }
+  },
+});
+
+/**
+ * We now add this handler to our config
+ */
+export const config: VendureConfig = {
+  // ...
+  paymentOptions: {
+    paymentMethodHandlers: [myPaymentIntegration],
+  },
+};
+```
+
+## Payment flow
+
+1. Once the active Order has been transitioned to the ArrangingPayment state (see the [Order Workflow guide]({{< relref "order-workflow" >}})), one or more Payments are created by executing the [`addPaymentToOrder` mutation]({{< relref "/docs/graphql-api/shop/mutations#addpaymenttoorder" >}}). This mutation has a required `method` input field, which _must_ match the `code` of one of the configured PaymentMethodHandlers. In the case above, this would be set to `"my-payment-method"`.
+    ```GraphQL
+    mutation {
+        addPaymentToOrder(input: { 
+            method: "my-payment-method",
+            metadata: { token: "<some token from the payment provider>" }) {
+            ...Order
+        }
+    }
+    ```
+   The `metadata` field is used to store the specific data required by the payment provider. E.g. some providers have a client-side part which begins the transaction and returns a token which must then be verified on the server side.
+2. This mutation internally invokes the [PaymentMethodHandler's `createPayment()` function]({{< relref "payment-method-config-options" >}}#createpayment). This function returns a [CreatePaymentResult object]({{< relref "payment-method-types" >}}#payment-method-types) which is used to create a new [Payment]({{< relref "/docs/typescript-api/entities/payment" >}}). If the Payment amount equals the order total, then the Order is transitioned to either the "PaymentAuthorized" or "PaymentSettled" state and the customer checkout flow is complete.
+
+### Single-step
+
+If the `createPayment()` function returns a result with the state set to `'Settled'`, then this is a single-step ("authorize & capture") flow, as illustrated below:
+
+{{< figure src="./payment_sequence_one_step.png" >}}
+
+### Two-step
+
+If the `createPayment()` function returns a result with the state set to `'Authorized'`, then this is a two-step flow, and the settlement / capture part is performed at some later point, e.g. when shipping the goods, or on confirmation of payment-on-delivery.
+
+{{< figure src="./payment_sequence_two_step.png" >}}
+

BIN
docs/content/docs/developer-guide/payment-integrations/payment_sequence_one_step.png


BIN
docs/content/docs/developer-guide/payment-integrations/payment_sequence_two_step.png


+ 2 - 0
docs/content/docs/storefront/shop-api-guide.md

@@ -42,6 +42,8 @@ There are a couple of query parameters which are valid for all GraphQL operation
 * {{< shop-api-operation operation="setOrderShippingAddress" type="mutation" >}} sets the shipping address for the Order.
 * {{< shop-api-operation operation="eligibleShippingMethods" type="mutation" >}} returns all available shipping methods based on the customer's shipping address and the contents of the Order.
 * {{< shop-api-operation operation="setOrderShippingMethod" type="mutation" >}} sets the shipping method to use.
+* {{< shop-api-operation operation="nextOrderStates" type="query" >}} returns the possible next states that the active Order can transition to
+* {{< shop-api-operation operation="transitionOrderToState" type="mutation" >}} transitions the active Order to the given state according to the [Order state machine]({{< relref "order-workflow" >}}).
 * {{< shop-api-operation operation="addPaymentToOrder" type="mutation" >}} adds a payment to the Order. If the payment amount equals the order total, then the Order will be transitioned to either the `PaymentAuthorized` or `PaymentSettled` state (depending on how the payment provider is configured) and the order process is complete from the customer's side.
 * {{< shop-api-operation operation="orderByCode" type="query" >}} allows even guest Customers to fetch the order they just placed for up to 2 hours after placing it. This is intended to be used to display an order confirmation page immediately after the order is completed.
 

+ 21 - 0
docs/diagrams/payment-sequence-one-step.puml

@@ -0,0 +1,21 @@
+@startuml
+!include theme.puml
+title Payment Integration - single-step flow
+skinparam SequenceBoxBorderColor #
+hide footbox
+participant Storefront #555
+box "Vendure Server" #Lightblue
+participant "Shop API" as ShopAPI
+participant PaymentMethodHandler
+end box
+participant "Payment Provider" as PaymentProvider #39a4ac
+
+Storefront -> ShopAPI: **addPaymentToOrder**\nmutation
+ShopAPI -> PaymentMethodHandler++
+PaymentMethodHandler -> PaymentProvider: **createPayment()**
+note right: The Payment Provider **authorizes**\n__and__ **captures** the payment
+PaymentProvider --> PaymentMethodHandler: Transaction ID
+PaymentMethodHandler --> ShopAPI: creates new **Payment**\nfor the Order
+deactivate PaymentMethodHandler
+ShopAPI --> Storefront: Order in\n**PaymentSettled** state
+@enduml

+ 28 - 0
docs/diagrams/payment-sequence-two-step.puml

@@ -0,0 +1,28 @@
+@startuml
+!include theme.puml
+title Payment Integration - two-step flow
+skinparam SequenceBoxBorderColor #
+hide footbox
+participant Storefront #555
+box "Vendure Server" #Lightblue
+participant "Shop API" as ShopAPI
+participant "Admin API" as AdminAPI
+participant PaymentMethodHandler
+end box
+participant "Payment Provider" as PaymentProvider #39a4ac
+
+Storefront -> ShopAPI: **addPaymentToOrder**\nmutation
+ShopAPI -> PaymentMethodHandler++
+PaymentMethodHandler -> PaymentProvider: **createPayment()**
+note right: The Payment Provider\n**authorizes** the payment
+PaymentProvider --> PaymentMethodHandler: Transaction ID
+PaymentMethodHandler --> ShopAPI: creates new **Payment**\nfor the Order
+deactivate PaymentMethodHandler
+ShopAPI --> Storefront: Order in\n**PaymentAuthorized** state
+== Second step done via Admin UI / Admin API ==
+AdminAPI -> PaymentMethodHandler: **settlePayment** mutation
+PaymentMethodHandler -> PaymentProvider: **settlePayment()**
+note right: The Payment Provider\n**captures** the payment
+PaymentProvider --> PaymentMethodHandler: Confirmation data
+PaymentMethodHandler --> AdminAPI: Order in\n**PaymentSettled** state
+@enduml

+ 3 - 3
packages/core/src/config/payment-method/payment-method-handler.ts

@@ -185,21 +185,21 @@ export interface PaymentMethodConfigOptions<T extends ConfigArgs> extends Config
  *             });
  *             return {
  *                 amount: order.total,
- *                 state: 'Settled' as 'Settled',
+ *                 state: 'Settled' as const,
  *                 transactionId: result.id.toString(),
  *                 metadata: result.outcome,
  *             };
  *         } catch (err) {
  *             return {
  *                 amount: order.total,
- *                 state: 'Declined' as 'Declined',
+ *                 state: 'Declined' as const,
  *                 metadata: {
  *                     errorMessage: err.message,
  *                 },
  *             };
  *         }
  *     },
- *     settlePayment: async (order, payment, args): Promise<SettlePaymentResult> {
+ *     settlePayment: async (order, payment, args): Promise<SettlePaymentResult> => {
  *         return { success: true };
  *     }
  * });