--- title: "Custom Template Loader" weight: 10 date: 2023-07-14T16:57:50.756Z showtoc: true generated: true --- # Custom Template Loader
) { super(input); } @ManyToOne(type => Order) order: Order; @EntityId() orderId: ID; @Column() text: string; // highlight-start @Money() value: number; // highlight-end // Whenever you store a monetary value, it's a good idea to also // explicitly store the currency code too. This makes it possible // to support multiple currencies and correctly format the amount // when displaying the value. @Column('varchar') currencyCode: CurrencyCode; @Column() approved: boolean; } ``` ## Advanced configuration: MoneyStrategy For advanced use-cases, it is possible to configure aspects of how Vendure handles monetary values internally by defining a custom [`MoneyStrategy`](/reference/typescript-api/money/money-strategy/). The `MoneyStrategy` allows you to define: - How the value is stored and retrieved from the database - How rounding is applied internally - The precision represented by the monetary value (since v2.2.0) For example, in addition to the [`DefaultMoneyStrategy`](/reference/typescript-api/money/default-money-strategy), Vendure also provides the [`BigIntMoneyStrategy`](/reference/typescript-api/money/big-int-money-strategy) which stores monetary values using the `bigint` data type, allowing much larger amounts to be stored. Here's how you would configure your server to use this strategy: ```ts title="src/vendure-config.ts" import { VendureConfig, BigIntMoneyStrategy } from '@vendure/core'; export const config: VendureConfig = { // ... entityOptions: { moneyStrategy: new BigIntMoneyStrategy(), } } ``` ### Example: supporting three decimal places Let's say you have a B2B store which sells products in bulk, and you want to support prices with three decimal places. For example, you want to be able to sell a product for `$1.234` per unit. To do this, you would need to: 1. Configure the `MoneyStrategy` to use three decimal places ```ts import { DefaultMoneyStrategy, VendureConfig } from '@vendure/core'; export class ThreeDecimalPlacesMoneyStrategy extends DefaultMoneyStrategy { // highlight-next-line readonly precision = 3; } export const config: VendureConfig = { // ... entityOptions: { moneyStrategy: new ThreeDecimalPlacesMoneyStrategy(), } }; ``` 2. Set up your storefront to correctly convert the integer value to a decimal value with three decimal places. Using the `formatCurrency` example above, we can modify it to divide by 1000 instead of 100: ```ts title="src/utils/format-currency.ts" export function formatCurrency(value: number, currencyCode: string, locale?: string) { // highlight-next-line const majorUnits = value / 1000; try { return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode, // highlight-start minimumFractionDigits: 3, maximumFractionDigits: 3, // highlight-end }).format(majorUnits); } catch (e: any) { // highlight-next-line return majorUnits.toFixed(3); } } ``` --- --- title: 'Orders' --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; In Vendure, the [`Order`](/reference/typescript-api/entities/order/) entity represents the entire lifecycle of an order, from the moment a customer adds an item to their cart, through to the point where the order is completed and the customer has received their goods. An [`Order`](/reference/typescript-api/entities/order/) is composed of one or more [`OrderLines`](/reference/typescript-api/entities/order-line/). Each order line represents a single product variant, and contains information such as the quantity, price, tax rate, etc. In turn, the order is associated with a [`Customer`](/reference/typescript-api/entities/customer/) and contains information such as the shipping address, billing address, shipping method, payment method, etc.  ## The Order Process Vendure defines an order process which is based on a [finite state machine](/reference/typescript-api/state-machine/fsm/) (a method of precisely controlling how the order moves from one state to another). This means that the [`Order.state` property](/reference/typescript-api/entities/order/#state) will be one of a set of [pre-defined states](/reference/typescript-api/orders/order-process/#orderstate). From the current state, the Order can then transition (change) to another state, and the available next states depend on what the current state is. :::note In Vendure, there is no distinction between a "cart" and an "order". The same entity is used for both. A "cart" is simply an order which is still "active" according to its current state. ::: You can see the current state of an order via `state` field on the `Order` type:The next possible states can be queried via the [`nextOrderStates`](/reference/graphql-api/shop/queries/#nextorderstates) query: ```graphql title="Shop API" query ActiveOrder { activeOrder { id // highlight-next-line state } } ``` ```json { "data": { "activeOrder": { "id": "4", // highlight-next-line "state": "AddingItems" } } } ``` The available states and the permissible transitions between them are defined by the configured [`OrderProcess`](/reference/typescript-api/orders/order-process/). By default, Vendure defines a [`DefaultOrderProcess`](/reference/typescript-api/orders/order-process/#defaultorderprocess) which is suitable for typical B2C use-cases. Here's a simplified diagram of the default order process:  Let's take a look at each of these states, and the transitions between them: * **`AddingItems:`** All orders begin in the `AddingItems` state. This means that the customer is adding items to his or her shopping cart. This is the state an order would be in as long as the customer is still browsing the store. * **`ArrangingPayment:`** From there, the Order can transition to the `ArrangingPayment`, which will prevent any further modifications to the order, which ensures the price that is sent to the payment provider is the same as the price that the customer saw when they added the items to their cart. At this point, the storefront will execute the [`addPaymentToOrder` mutation](/reference/graphql-api/shop/mutations/#addpaymenttoorder). * **`PaymentAuthorized:`** Depending on the configured payment method, the order may then transition to the `PaymentAuthorized` state, which indicates that the payment has been successfully authorized by the payment provider. This is the state that the order will be in if the payment is not captured immediately. Once the payment is captured, the order will transition to the `PaymentSettled` state. * **`PaymentSettled:`** If the payment captured immediately, the order will transition to the `PaymentSettled` state once the payment succeeds. * At this point, one or more fulfillments can be created. A `Fulfillment` represents the process of shipping one or more items to the customer ("shipping" applies equally to physical or digital goods - it just means getting the product to the customer by any means). A fulfillment can be created via the [`addFulfillmentToOrder` mutation](/reference/graphql-api/admin/mutations/#addfulfillmenttoorder), or via the Admin UI. If multiple fulfillments are created, then the order can end up partial states - `PartiallyShipped` or `PartiallyDelivered`. If there is only a single fulfillment which includes the entire order, then partial states are not possible. * **`Shipped:`** When all fulfillments have been shipped, the order will transition to the `Shipped` state. This means the goods have left the warehouse and are en route to the customer. * **`Delivered:`** When all fulfillments have been delivered, the order will transition to the `Delivered` state. This means the goods have arrived at the customer's address. This is the final state of the order. ## Customizing the Default Order Process It is possible to customize the [defaultOrderProcess](/reference/typescript-api/orders/order-process/#defaultorderprocess) to better match your business needs. For example, you might want to disable some of the constraints that are imposed by the default process, such as the requirement that a customer must have a shipping address before the Order can be completed. This can be done by creating a custom version of the default process using the [configureDefaultOrderProcess](/reference/typescript-api/orders/order-process/#configuredefaultorderprocess) function, and then passing it to the [`OrderOptions.process`](/reference/typescript-api/orders/order-options/#process) config property. ```ts title="src/vendure-config.ts" import { configureDefaultOrderProcess, VendureConfig } from '@vendure/core'; const myCustomOrderProcess = configureDefaultOrderProcess({ // Disable the constraint that requires // Orders to have a shipping method assigned // before payment. arrangingPaymentRequiresShipping: false, // Other constraints which can be disabled. See the // DefaultOrderProcessOptions interface docs for full // explanations. // // checkModificationPayments: false, // checkAdditionalPaymentsAmount: false, // checkAllVariantsExist: false, // arrangingPaymentRequiresContents: false, // arrangingPaymentRequiresCustomer: false, // arrangingPaymentRequiresStock: false, // checkPaymentsCoverTotal: false, // checkAllItemsBeforeCancel: false, // checkFulfillmentStates: false, }); export const config: VendureConfig = { orderOptions: { process: [myCustomOrderProcess], }, }; ``` ## Custom Order Processes Sometimes you might need to extend things beyond what is provided by the default Order process to better match your business needs. This is done by defining one or more [`OrderProcess`](/reference/typescript-api/orders/order-process#orderprocess) objects and passing them to the [`OrderOptions.process`](/reference/typescript-api/orders/order-options/#process) config property. ### Adding a new state Let's say your company can only sell to customers with a valid EU tax ID. We'll assume that you've already used a [custom field](/guides/developer-guide/custom-fields/) to store that code on the Customer entity. Now you want to add a step _before_ the customer handles payment, where we can collect and verify the tax ID. So we want to change the default process of: ```text AddingItems -> ArrangingPayment ``` to instead be: ```text AddingItems -> ValidatingCustomer -> ArrangingPayment ``` Here's how we would define the new state: ```ts title="src/plugins/tax-id/customer-validation-process.ts" import { OrderProcess } from '@vendure/core'; export const customerValidationProcess: OrderProcess<'ValidatingCustomer'> = { transitions: { AddingItems: { to: ['ValidatingCustomer'], mergeStrategy: 'replace', }, ValidatingCustomer: { to: ['ArrangingPayment', 'AddingItems'], }, }, }; ``` This object means: * the `AddingItems` state may _only_ transition to the `ValidatingCustomer` state (`mergeStrategy: 'replace'` tells Vendure to discard any existing transition targets and replace with this one). * the `ValidatingCustomer` may transition to the `ArrangingPayment` state (assuming the tax ID is valid) or back to the `AddingItems` state. And then add this configuration to our main VendureConfig: ```ts title="src/vendure-config.ts" import { defaultOrderProcess, VendureConfig } from '@vendure/core'; import { customerValidationProcess } from './plugins/tax-id/customer-validation-process'; export const config: VendureConfig = { // ... orderOptions: { process: [defaultOrderProcess, customerValidationProcess], }, }; ``` Note that we also include the `defaultOrderProcess` in the array, otherwise we will lose all the default states and transitions. To add multiple new States you need to extend the generic type like this: ```ts import { OrderProcess } from '@vendure/core'; export const customerValidationProcess: OrderProcess<'ValidatingCustomer'|'AnotherState'> = {...} ``` This way multiple custom states get defined. ### Intercepting a state transition Now we have defined our new `ValidatingCustomer` state, but there is as yet nothing to enforce that the tax ID is valid. To add this constraint, we'll use the [`onTransitionStart` state transition hook](/reference/typescript-api/state-machine/state-machine-config#ontransitionstart). This allows us to perform our custom logic and potentially prevent the transition from occurring. We will also assume that we have a provider named `TaxIdService` available which contains the logic to validate a tax ID. ```ts title="src/plugins/tax-id/customer-validation-process.ts" import { OrderProcess } from '@vendure/core'; import { TaxIdService } from './services/tax-id.service'; let taxIdService: TaxIdService; const customerValidationProcess: OrderProcess<'ValidatingCustomer'> = { transitions: { AddingItems: { to: ['ValidatingCustomer'], mergeStrategy: 'replace', }, ValidatingCustomer: { to: ['ArrangingPayment', 'AddingItems'], }, }, init(injector) { taxIdService = injector.get(TaxIdService); }, // The logic for enforcing our validation goes here async onTransitionStart(fromState, toState, data) { if (fromState === 'ValidatingCustomer' && toState === 'ArrangingPayment') { const isValid = await taxIdService.verifyTaxId(data.order.customer); if (!isValid) { // Returning a string is interpreted as an error message. // The state transition will fail. return `The tax ID is not valid`; } } }, }; ``` :::info For an explanation of the `init()` method and `injector` argument, see the guide on [injecting dependencies in configurable operations](/guides/developer-guide/strategies-configurable-operations/#injecting-dependencies). ::: ### Responding to a state transition Once an order has successfully transitioned to a new state, the [`onTransitionEnd` state transition hook](/reference/typescript-api/state-machine/state-machine-config#ontransitionend) is called. This can be used to perform some action upon successful state transition. In this example, we have a referral service which creates a new referral for a customer when they complete an order. We want to create the referral only if the customer has a referral code associated with their account. ```ts import { OrderProcess, OrderState } from '@vendure/core'; import { ReferralService } from '../service/referral.service'; let referralService: ReferralService; export const referralOrderProcess: OrderProcess ```graphql title="Shop API" query NextStates { nextOrderStates } ``` ```json { "data": { "nextOrderStates": [ "ArrangingPayment", "Cancelled" ] } } ``` = { init: (injector) => { referralService = injector.get(ReferralService); }, onTransitionEnd: async (fromState, toState, data) => { const { order, ctx } = data; if (toState === 'PaymentSettled') { if (order.customFields.referralCode) { await referralService.createReferralForOrder(ctx, order); } } }, }; ``` :::caution Use caution when modifying an order inside the `onTransitionEnd` function. The `order` object that gets passed in to this function will later be persisted to the database. Therefore any changes must be made to that `order` object, otherwise the changes might be lost. As an example, let's say we want to add a Surcharge to the order. The following code **will not work as expected**: ```ts export const myOrderProcess: OrderProcess = { async onTransitionEnd(fromState, toState, data) { if (fromState === 'AddingItems' && toState === 'ArrangingPayment') { // highlight-start // WARNING: This will not work! await orderService.addSurchargeToOrder(ctx, order.id, { description: 'Test', listPrice: 42, listPriceIncludesTax: false, }); // highlight-end } } }; ``` Instead, you need to ensure you **mutate the `order` object**: ```ts export const myOrderProcess: OrderProcess = { async onTransitionEnd(fromState, toState, data) { if (fromState === 'AddingItems' && toState === 'ArrangingPayment') { // highlight-start const {surcharges} = await orderService.addSurchargeToOrder(ctx, order.id, { description: 'Test', listPrice: 42, listPriceIncludesTax: false, }); // Important: mutate the order object order.surcharges = surcharges; // highlight-end } }, } ``` ::: ## TypeScript Typings To make your custom states compatible with standard services you should declare your new states in the following way: ```ts title="src/plugins/tax-id/types.ts" import { CustomOrderStates } from '@vendure/core'; declare module '@vendure/core' { interface CustomOrderStates { ValidatingCustomer: never; } } ``` This technique uses advanced TypeScript features - [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) and [ambient modules](https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules). ## Controlling custom states in the Admin UI If you have defined custom order states, the Admin UI will allow you to manually transition an order from one state to another:  ## Order Interceptors Vendure v3.1 introduces the concept of [Order Interceptors](/reference/typescript-api/orders/order-interceptor/). These are a way to intercept operations that add, modify or remove order lines. Examples use-cases include: * Preventing certain products from being added to the order based on some criteria, e.g. if the product is already in another active order. * Enforcing a minimum or maximum quantity of a given product in the order * Using a CAPTCHA to prevent automated order creation Check the [Order Interceptor](/reference/typescript-api/orders/order-interceptor/) docs for more information as well as a complete example of how to implement an interceptor. --- --- title: 'Payment' --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; 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. :::info For complete working examples of real payment integrations, see the [payments-plugins](https://github.com/vendure-ecommerce/vendure/tree/master/packages/payments-plugin/src) ::: ## 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](/reference/typescript-api/payment/payment-method-handler/) and passing that handler into the [`paymentOptions.paymentMethodHandlers`](/reference/typescript-api/payment/payment-options#paymentmethodhandlers) array in the VendureConfig. ```ts title="src/plugins/payment-plugin/my-payment-handler.ts" import { CancelPaymentResult, CancelPaymentErrorResult, PaymentMethodHandler, VendureConfig, CreatePaymentResult, SettlePaymentResult, SettlePaymentErrorResult } 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 myPaymentHandler = 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 (ctx, order, amount, args, metadata): Promise => { try { const result = await sdk.charges.create({ amount, apiKey: args.apiKey, source: metadata.token, }); return { amount: order.total, state: 'Authorized' as const, transactionId: result.id.toString(), metadata: { cardInfo: result.cardInfo, // Any metadata in the `public` field // will be available in the Shop API, // All other metadata is private and // only available in the Admin API. public: { referenceCode: result.publicId, } }, }; } 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 (ctx, order, payment, args): Promise => { try { const result = await sdk.charges.capture({ apiKey: args.apiKey, id: payment.transactionId, }); return {success: true}; } catch (err) { return { success: false, errorMessage: err.message, } } }, /** This is called when a payment is cancelled. */ cancelPayment: async (ctx, order, payment, args): Promise => { try { const result = await sdk.charges.cancel({ apiKey: args.apiKey, id: payment.transactionId, }); return {success: true}; } catch (err) { return { success: false, errorMessage: err.message, } } }, }); ``` We can now add this handler to our configuration: ```ts title="src/vendure-config.ts" import { VendureConfig } from '@vendure/core'; import { myPaymentHandler } from './plugins/payment-plugin/my-payment-handler'; export const config: VendureConfig = { // ... paymentOptions: { paymentMethodHandlers: [myPaymentHandler], }, }; ``` :::info If your PaymentMethodHandler needs access to the database or other providers, see the [configurable operation dependency injection guide](/guides/developer-guide/strategies-configurable-operations/#injecting-dependencies). ::: ## The PaymentMethod entity Once the PaymentMethodHandler is defined as above, you can use it to create a new [`PaymentMethod`](/reference/typescript-api/entities/payment-method/) via the Admin UI (_Settings_ -> _Payment methods_, then _Create new payment method_) or via the Admin API `createPaymentMethod` mutation. A payment method consists of an optional [`PaymentMethodEligibilityChecker`](/reference/typescript-api/payment/payment-method-eligibility-checker/), which is used to determine whether the payment method is available to the customer, and a [`PaymentMethodHandler`](/reference/typescript-api/payment/payment-method-handler). The payment method also has a **code**, which is a string identifier used to specify this method when adding a payment to an order.  ## Payment flow ### Eligible payment methods Once the active Order has been transitioned to the `ArrangingPayment` state (see the [Order guide](/guides/core-concepts/orders/)), we can query the available payment methods by executing the [`eligiblePaymentMethods` query](/reference/graphql-api/shop/queries#eligiblepaymentmethods). ### Add payment to order One or more Payments are created by executing the [`addPaymentToOrder` mutation](/reference/graphql-api/shop/mutations#addpaymenttoorder). This mutation has a required `method` input field, which _must_ match the `code` of an eligible `PaymentMethod`. In the case above, this would be set to `"my-payment-method"`. ```graphql title="Shop API" query GetEligiblePaymentMethods { eligiblePaymentMethods { code name isEligible eligibilityMessage } } ``` ```json { "data": { "eligiblePaymentMethods": [ { "code": "my-payment-method", "name": "My Payment Method", "isEligible": true, "eligibilityMessage": null } ] } } ``` :::info 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. The `metadata` field is required, so if your payment provider does not require any additional data, you can simply pass an empty object: `metadata: {}`. ::: 3. This mutation internally invokes the [PaymentMethodHandler's `createPayment()` function](/reference/typescript-api/payment/payment-method-config-options/#createpayment). This function returns a [CreatePaymentResult object](/reference/typescript-api/payment/payment-method-types#createpaymentfn) which is used to create a new [Payment](/reference/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:  ### 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.  ## Custom Payment Flows If you need to support an entirely different payment flow than the above, it is also possible to do so by configuring a [PaymentProcess](/reference/typescript-api/payment/payment-process). This allows new Payment states and transitions to be defined, as well as allowing custom logic to run on Payment state transitions. Here's an example which adds a new "Validating" state to the Payment state machine, and combines it with a [OrderProcess](/reference/typescript-api/orders/order-process), [PaymentMethodHandler](/reference/typescript-api/payment/payment-method-handler) and [OrderPlacedStrategy](/reference/typescript-api/orders/order-placed-strategy). ```text ├── plugins └── my-payment-plugin ├── payment-process.ts ├── payment-method-handler.ts ├── order-process.ts └── order-placed-strategy.ts ``` ```ts title="src/plugins/my-payment-plugin/payment-process.ts" import { PaymentProcess } from '@vendure/core'; /** * Declare your custom state in special interface to make it type-safe */ declare module '@vendure/core' { interface PaymentStates { Validating: never; } } /** * Define a new "Validating" Payment state, and set up the * permitted transitions to/from it. */ const customPaymentProcess: PaymentProcess<'Validating'> = { transitions: { Created: { to: ['Validating'], mergeStrategy: 'replace', }, Validating: { to: ['Settled', 'Declined', 'Cancelled'], }, }, }; ``` ```ts title="src/plugins/my-payment-plugin/order-process.ts" import { OrderProcess } from '@vendure/core'; /** * Define a new "ValidatingPayment" Order state, and set up the * permitted transitions to/from it. */ const customOrderProcess: OrderProcess<'ValidatingPayment'> = { transitions: { ArrangingPayment: { to: ['ValidatingPayment'], mergeStrategy: 'replace', }, ValidatingPayment: { to: ['PaymentAuthorized', 'PaymentSettled', 'ArrangingAdditionalPayment'], }, }, }; ``` ```ts title="src/plugins/my-payment-plugin/payment-method-handler.ts" import { LanguageCode, PaymentMethodHandler } from '@vendure/core'; /** * This PaymentMethodHandler creates the Payment in the custom "Validating" * state. */ const myPaymentHandler = new PaymentMethodHandler({ code: 'my-payment-handler', description: [{languageCode: LanguageCode.en, value: 'My payment handler'}], args: {}, createPayment: (ctx, order, amount, args, metadata) => { // payment provider logic omitted return { state: 'Validating' as any, amount, metadata, }; }, settlePayment: (ctx, order, payment) => { return { success: true, }; }, }); ``` ```ts title="src/plugins/my-payment-plugin/order-placed-strategy.ts" import { OrderPlacedStrategy, OrderState, RequestContext } from '@vendure/core'; /** * This OrderPlacedStrategy tells Vendure to set the Order as "placed" * when it transitions to the custom "ValidatingPayment" state. */ class MyOrderPlacedStrategy implements OrderPlacedStrategy { shouldSetAsPlaced(ctx: RequestContext, fromState: OrderState, toState: OrderState): boolean | Promise ```graphql title="Shop API" mutation { addPaymentToOrder( input: { method: "my-payment-method" metadata: { token: " " } } ) { ... on Order { id code state # ... etc } ... on ErrorResult { errorCode message } ...on PaymentFailedError { paymentErrorMessage } ...on PaymentDeclinedError { paymentErrorMessage } ...on IneligiblePaymentMethodError { eligibilityCheckerMessage } } } ``` ```json { "data": { "addPaymentToOrder": { "id": "12345", "code": "J9AC5PY13BQGRKTF", "state": "PaymentAuthorized" } } } ``` { return fromState === 'ArrangingPayment' && toState === ('ValidatingPayment' as any); } } ``` ```ts title="src/vendure-config.ts" import { defaultOrderProcess, defaultPaymentProcess, VendureConfig } from '@vendure/core'; import { customOrderProcess } from './plugins/my-payment-plugin/order-process'; import { customPaymentProcess } from './plugins/my-payment-plugin/payment-process'; import { myPaymentHandler } from './plugins/my-payment-plugin/payment-method-handler'; import { MyOrderPlacedStrategy } from './plugins/my-payment-plugin/order-placed-strategy'; // Combine the above in the VendureConfig export const config: VendureConfig = { // ... orderOptions: { process: [defaultOrderProcess, customOrderProcess], orderPlacedStrategy: new MyOrderPlacedStrategy(), }, paymentOptions: { process: [defaultPaymentProcess, customPaymentProcess], paymentMethodHandlers: [myPaymentHandler], }, }; ``` ### Integration with hosted payment pages A hosted payment page is a system that works similar to [Stripe checkout](https://stripe.com/payments/checkout). The idea behind this flow is that the customer does not enter any credit card data anywhere on the merchant's site which waives the merchant from the responsibility to take care of sensitive data. The checkout flow works as follows: 1. The user makes a POST to the card processor's URL via a Vendure served page 2. The card processor accepts card information from the user and authorizes a payment 3. The card processor redirects the user back to Vendure via a POST which contains details about the processed payment 4. There is a pre-shared secret between the merchant and processor used to sign cross-site POST requests When integrating with a system like this, you would need to create a Controller to accept POST redirects from the payment processor (usually a success and a failure URL), as well as serve a POST form on your store frontend. With a hosted payment form the payment is already authorized by the time the card processor makes the POST request to Vendure, possibly settled even, so the payment handler won't do anything in particular - just return the data it has been passed. The validation of the POST request is done in the controller or service and the payment amount and payment reference are just passed to the payment handler which passes them on. --- --- title: "Products" --- Your catalog is composed of [`Products`](/reference/typescript-api/entities/product/) and [`ProductVariants`](/reference/typescript-api/entities/product-variant/). A `Product` always has _at least one_ `ProductVariant`. You can think of the product as a "container" which includes a name, description, and images that apply to all of its variants. Here's a visual example, in which we have a "Hoodie" product which is available in 3 sizes. Therefore, we have 3 variants of that product:  Multiple variants are made possible by adding one or more [`ProductOptionGroups`](/reference/typescript-api/entities/product-option-group) to the product. These option groups then define the available [`ProductOptions`](/reference/typescript-api/entities/product-option) If we were to add a new option group to the example above for "Color", with 2 options, "Black" and "White", then in total we would be able to define up to 6 variants: - Hoodie Small Black - Hoodie Small White - Hoodie Medium Black - Hoodie Medium White - Hoodie Large Black - Hoodie Large White :::info When a customer adds a product to their cart, they are adding a specific `ProductVariant` to their cart, not the `Product` itself. It is the `ProductVariant` that contains the SKU ("stock keeping unit", or product code) and price information. ::: ## Product price and stock The `ProductVariant` entity contains the price and stock information for a product. Since a given product variant can have more than one price, and more than one stock level (in the case of multiple warehouses), the `ProductVariant` entity contains relations to one or more [`ProductVariantPrice`](/reference/typescript-api/entities/product-variant-price) entities and one or more [`StockLevel`](/reference/typescript-api/entities/stock-level) entities.  ## Facets [`Facets`](/reference/typescript-api/entities/facet/) are used to add structured labels to products and variants. A facet has one or more [`FacetValues`](/reference/typescript-api/entities/facet-value/). Facet values can be assigned to products or product variants. For example, a "Brand" facet could be used to label products with the brand name, with each facet value representing a different brand. You can also use facets to add other metadata to products and variants such as "New", "Sale", "Featured", etc.  These are the typical uses of facets in Vendure: - As the **basis of [Collections](/guides/core-concepts/collections)**, in order to categorize your catalog. - To **filter products** in the storefront, also known as "faceted search". For example, a customer is on the "hoodies" collection page and wants to filter to only show Nike hoodies. - For **internal logic**, such as a promotion that applies to all variants with the "Summer Sale" facet value, or a shipping calculation that applies a surcharge to all products with the "Fragile" facet value. Such facets can be set to be private so that they are not exposed to the storefront. --- --- title: 'Promotions' --- Promotions are a means of offering discounts on an order based on various criteria. A Promotion consists of _conditions_ and _actions_. - **conditions** are the rules which determine whether the Promotion should be applied to the order. - **actions** specify exactly how this Promotion should modify the order. ## Parts of a Promotion ### Constraints All Promotions can have the following constraints applied to them: - **Date range** Using the "starts at" and "ends at" fields, the Promotion can be scheduled to only be active during the given date range. - **Coupon code** A Promotion can require a coupon code first be activated using the [`applyCouponCode` mutation](/reference/graphql-api/shop/mutations/#applycouponcode) in the Shop API. - **Per-customer limit** A Promotion coupon may be limited to a given number of uses per Customer. ### Conditions A Promotion may be additionally constrained by one or more conditions. When evaluating whether a Promotion should be applied, each of the defined conditions is checked in turn. If all the conditions evaluate to `true`, then any defined actions are applied to the order. Vendure comes with some built-in conditions, but you can also create your own conditions (see section below). ### Actions A promotion action defines exactly how the order discount should be calculated. **At least one** action must be specified for a valid Promotion. Vendure comes with some built-in actions, but you can also create your own actions (see section below). ## Creating custom conditions To create a custom condition, you need to define a new [`PromotionCondition` object](/reference/typescript-api/promotions/promotion-condition/). A promotion condition is an example of a [configurable operation](/guides/developer-guide/strategies-configurable-operations/#configurable-operations). Here is an annotated example of one of the built-in PromotionConditions. ```ts import { LanguageCode, PromotionCondition } from '@vendure/core'; export const minimumOrderAmount = new PromotionCondition({ /** A unique identifier for the condition */ code: 'minimum_order_amount', /** * A human-readable description. Values defined in the * `args` object can be interpolated using the curly-braces syntax. */ description: [ {languageCode: LanguageCode.en, value: 'If order total is greater than { amount }'}, ], /** * Arguments which can be specified when configuring the condition * in the Admin UI. The values of these args are then available during * the execution of the `check` function. */ args: { amount: { type: 'int', // The optional `ui` object allows you to customize // how this arg is rendered in the Admin UI. ui: {component: 'currency-form-input'}, }, taxInclusive: {type: 'boolean'}, }, /** * This is the business logic of the condition. It is a function that * must resolve to a boolean value indicating whether the condition has * been satisfied. */ check(ctx, order, args) { if (args.taxInclusive) { return order.subTotalWithTax >= args.amount; } else { return order.subTotal >= args.amount; } }, }); ``` Custom promotion conditions are then passed into the VendureConfig [PromotionOptions](/reference/typescript-api/promotions/promotion-options/) to make them available when setting up Promotions: ```ts title="src/vendure-config.ts" import { defaultPromotionConditions, VendureConfig } from '@vendure/core'; import { minimumOrderAmount } from './minimum-order-amount'; export const config: VendureConfig = { // ... promotionOptions: { promotionConditions: [ ...defaultPromotionConditions, minimumOrderAmount, ], } } ``` ## Creating custom actions There are three kinds of PromotionAction: - [`PromotionItemAction`](/reference/typescript-api/promotions/promotion-action#promotionitemaction) applies a discount on the `OrderLine` level, i.e. it would be used for a promotion like "50% off USB cables". - [`PromotionOrderAction`](/reference/typescript-api/promotions/promotion-action#promotionorderaction) applies a discount on the `Order` level, i.e. it would be used for a promotion like "5% off the order total". - [`PromotionShippingAction`](/reference/typescript-api/promotions/promotion-action#promotionshippingaction) applies a discount on the shipping, i.e. it would be used for a promotion like "free shipping". The implementations of each type is similar, with the difference being the arguments passed to the `execute()`. Here's an example of a simple PromotionOrderAction. ```ts import { LanguageCode, PromotionOrderAction } from '@vendure/core'; export const orderPercentageDiscount = new PromotionOrderAction({ // See the custom condition example above for explanations // of code, description & args fields. code: 'order_percentage_discount', description: [{languageCode: LanguageCode.en, value: 'Discount order by { discount }%'}], args: { discount: { type: 'int', ui: { component: 'number-form-input', suffix: '%', }, }, }, /** * This is the function that defines the actual amount to be discounted. * It should return a negative number representing the discount in * pennies/cents etc. Rounding to an integer is handled automatically. */ execute(ctx, order, args) { const orderTotal = ctx.channel.pricesIncludeTax ? order.subTotalWithTax : order.subTotal; return -orderTotal * (args.discount / 100); }, }); ``` Custom PromotionActions are then passed into the VendureConfig [PromotionOptions](/reference/typescript-api/promotions/promotion-options) to make them available when setting up Promotions: ```ts title="src/vendure-config.ts" import { defaultPromotionActions, VendureConfig } from '@vendure/core'; import { orderPercentageDiscount } from './order-percentage-discount'; export const config: VendureConfig = { // ... promotionOptions: { promotionActions: [ ...defaultPromotionActions, orderPercentageDiscount, ], } }; ``` ## Free gift promotions Vendure v1.8 introduced a new **side effect API** to PromotionActions, which allow you to define some additional action to be performed when a Promotion becomes active or inactive. A primary use-case of this API is to add a free gift to the Order. Here's an example of a plugin which implements a "free gift" action: ```ts title="src/plugins/free-gift/free-gift.plugin.ts" import { ID, idsAreEqual, isGraphQlErrorResult, LanguageCode, Logger, OrderLine, OrderService, PromotionItemAction, VendurePlugin, } from "@vendure/core"; import { createHash } from "crypto"; let orderService: OrderService; export const freeGiftAction = new PromotionItemAction({ code: "free_gift", description: [{ languageCode: LanguageCode.en, value: "Add free gifts to the order" }], args: { productVariantIds: { type: "ID", list: true, ui: { component: "product-selector-form-input" }, label: [{ languageCode: LanguageCode.en, value: "Gift product variants" }], }, }, init(injector) { orderService = injector.get(OrderService); }, execute(ctx, orderLine, args) { // This part is responsible for ensuring the variants marked as // "free gifts" have their price reduced to zero if (lineContainsIds(args.productVariantIds, orderLine)) { const unitPrice = orderLine.productVariant.listPriceIncludesTax ? orderLine.unitPriceWithTax : orderLine.unitPrice; return -unitPrice; } return 0; }, // The onActivate function is part of the side effect API, and // allows us to perform some action whenever a Promotion becomes active // due to it's conditions & constraints being satisfied. async onActivate(ctx, order, args, promotion) { for (const id of args.productVariantIds) { if ( !order.lines.find( (line) => idsAreEqual(line.productVariant.id, id) && line.customFields.freeGiftPromotionId == null ) ) { // The order does not yet contain this free gift, so add it const result = await orderService.addItemToOrder(ctx, order.id, id, 1, { freeGiftPromotionId: promotion.id.toString(), }); if (isGraphQlErrorResult(result)) { Logger.error(`Free gift action error for variantId "${id}": ${result.message}`); } } } }, // The onDeactivate function is the other part of the side effect API and is called // when an active Promotion becomes no longer active. It should reverse any // side effect performed by the onActivate function. async onDeactivate(ctx, order, args, promotion) { const linesWithFreeGift = order.lines.filter( (line) => line.customFields.freeGiftPromotionId === promotion.id.toString() ); for (const line of linesWithFreeGift) { await orderService.removeItemFromOrder(ctx, order.id, line.id); } }, }); function lineContainsIds(ids: ID[], line: OrderLine): boolean { return !!ids.find((id) => idsAreEqual(id, line.productVariant.id)); } @VendurePlugin({ configuration: (config) => { config.customFields.OrderLine.push({ name: "freeGiftPromotionId", type: "string", public: true, readonly: true, nullable: true, }); config.customFields.OrderLine.push({ name: "freeGiftDescription", type: "string", public: true, readonly: true, nullable: true, }); config.promotionOptions.promotionActions.push(freeGiftAction); return config; }, }) export class FreeGiftPromotionPlugin {} ``` ## Dependency relationships It is possible to establish dependency relationships between a PromotionAction and one or more PromotionConditions. For example, if we want to set up a "buy 1, get 1 free" offer, we need to: 1. Establish whether the Order contains the particular ProductVariant under offer (done in the PromotionCondition) 2. Apply a discount to the qualifying OrderLine (done in the PromotionAction) In this scenario, we would have to repeat the logic for checking the Order contents in _both_ the PromotionCondition _and_ the PromotionAction. Not only is this duplicated work for the server, it also means that setting up the promotion relies on the same parameters being input into the PromotionCondition and the PromotionAction. Note the use of `PromotionItemAction` to get a reference to the `OrderLine` as opposed to the `Order`. Instead, we can say that the PromotionAction _depends_ on the PromotionCondition: ```ts export const buy1Get1FreeAction = new PromotionItemAction({ code: 'buy_1_get_1_free', description: [{ languageCode: LanguageCode.en, value: 'Buy 1, get 1 free', }], args: {}, // highlight-next-line conditions: [buyXGetYFreeCondition], execute(ctx, orderLine, args, state) { // highlight-next-line const freeItemIds = state.buy_x_get_y_free.freeItemIds; if (idsContainsItem(freeItemIds, orderLine)) { const unitPrice = ctx.channel.pricesIncludeTax ? orderLine.unitPriceWithTax : orderLine.unitPrice; return -unitPrice; } return 0; }, }); ``` In the above code, we are stating that this PromotionAction _depends_ on the `buyXGetYFreeCondition` PromotionCondition. Attempting to create a Promotion using the `buy1Get1FreeAction` without also using the `buyXGetYFreeCondition` will result in an error. In turn, the `buyXGetYFreeCondition` can return a _state object_ with the type `{ [key: string]: any; }` instead of just a `true` boolean value. This state object is then passed to the PromotionConditions which depend on it, as part of the last argument (`state`). ```ts export const buyXGetYFreeCondition = new PromotionCondition({ code: 'buy_x_get_y_free', description: [{ languageCode: LanguageCode.en, value: 'Buy { amountX } of { variantIdsX } products, get { amountY } of { variantIdsY } products free', }], args: { // omitted for brevity }, async check(ctx, order, args) { // logic omitted for brevity if (freeItemIds.length === 0) { return false; } // highlight-next-line return {freeItemIds}; }, }); ``` ## Injecting providers If your PromotionCondition or PromotionAction needs access to the database or other providers, they can be injected by defining an `init()` function in your PromotionAction or PromotionCondition. See the [configurable operation guide](/guides/developer-guide/strategies-configurable-operations/#injecting-dependencies) for details. --- --- title: "Shipping & Fulfillment" --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Shipping in Vendure is handled by [ShippingMethods](/reference/typescript-api/entities/shipping-method/). A ShippingMethod is composed of a **checker** and a **calculator**. * The [`ShippingEligibilityChecker`](/reference/typescript-api/shipping/shipping-eligibility-checker/) determines whether the order is eligible for the ShippingMethod. It can contain custom logic such as checking the total weight of the order, or whether the order is being shipped to a particular country. * The [`ShippingCalculator`](/reference/typescript-api/shipping/shipping-calculator/) calculates the cost of shipping the order. The calculation can be performed directly by the calculator itself, or it could call out to a third-party API to determine the cost.  Multiple shipping methods can be set up and then your storefront can query [`eligibleShippingMethods`](/reference/graphql-api/shop/queries/#eligibleshippingmethods) to find out which ones can be applied to the active order. When querying `eligibleShippingMethods`, each of the defined ShippingMethods' checker functions is executed to find out whether the order is eligible for that method, and if so, the calculator is executed to determine what the cost of shipping will be for that method. ## Creating a custom checker Custom checkers can be created by defining a [`ShippingEligibilityChecker` object](/reference/typescript-api/shipping/shipping-eligibility-checker/). For example, you could create a checker which works with a custom "weight" field to only apply to orders below a certain weight: ```ts title="src/shipping-methods/max-weight-checker.ts" import { LanguageCode, ShippingEligibilityChecker } from '@vendure/core'; export const maxWeightChecker = new ShippingEligibilityChecker({ code: 'max-weight-checker', description: [ {languageCode: LanguageCode.en, value: 'Max Weight Checker'} ], args: { maxWeight: { type: 'int', ui: {component: 'number-form-input', suffix: 'grams'}, label: [{languageCode: LanguageCode.en, value: 'Maximum order weight'}], description: [ { languageCode: LanguageCode.en, value: 'Order is eligible only if its total weight is less than the specified value', }, ], }, }, /** * Must resolve to a boolean value, where `true` means that the order is * eligible for this ShippingMethod. * * (This example assumes a custom field "weight" is defined on the * ProductVariant entity) */ check: (ctx, order, args) => { const totalWeight = order.lines .map(l => l.productVariant.customFields.weight ?? 0 * l.quantity) .reduce((total, lineWeight) => total + lineWeight, 0); return totalWeight <= args.maxWeight; }, }); ``` Custom checkers are then passed into the VendureConfig [ShippingOptions](/reference/typescript-api/shipping/shipping-options/#shippingeligibilitycheckers) to make them available when setting up new ShippingMethods: ```ts title="src/vendure-config.ts" import { defaultShippingEligibilityChecker, VendureConfig } from '@vendure/core'; import { maxWeightChecker } from './shipping-methods/max-weight-checker'; export const config: VendureConfig = { // ... shippingOptions: { shippingEligibilityCheckers: [ defaultShippingEligibilityChecker, maxWeightChecker, ], } } ``` ## Creating a custom calculator Custom calculators can be created by defining a [`ShippingCalculator` object](/reference/typescript-api/shipping/shipping-calculator/). For example, you could create a calculator which consults an external data source (e.g. a spreadsheet, database or 3rd-party API) to find out the cost and estimated delivery time for the order. ```ts title="src/shipping-methods/external-shipping-calculator.ts" import { LanguageCode, ShippingCalculator } from '@vendure/core'; import { shippingDataSource } from './shipping-data-source'; export const externalShippingCalculator = new ShippingCalculator({ code: 'external-shipping-calculator', description: [{languageCode: LanguageCode.en, value: 'Calculates cost from external source'}], args: { taxRate: { type: 'int', ui: {component: 'number-form-input', suffix: '%'}, label: [{languageCode: LanguageCode.en, value: 'Tax rate'}], }, }, calculate: async (ctx, order, args) => { // `shippingDataSource` is assumed to fetch the data from some // external data source. const { rate, deliveryDate, courier } = await shippingDataSource.getRate({ destination: order.shippingAddress, contents: order.lines, }); return { price: rate, priceIncludesTax: ctx.channel.pricesIncludeTax, taxRate: args.taxRate, // metadata is optional but can be used to pass arbitrary // data about the shipping estimate to the storefront. metadata: { courier, deliveryDate }, }; }, }); ``` Custom calculators are then passed into the VendureConfig [ShippingOptions](/reference/typescript-api/shipping/shipping-options/#shippingcalculators) to make them available when setting up new ShippingMethods: ```ts import { defaultShippingCalculator, VendureConfig } from '@vendure/core'; import { externalShippingCalculator } from './external-shipping-calculator'; export const config: VendureConfig = { // ... shippingOptions: { shippingCalculators: [ defaultShippingCalculator, externalShippingCalculator, ], } } ``` :::info If your ShippingEligibilityChecker or ShippingCalculator needs access to the database or other providers, see the [configurable operation dependency injection guide](/guides/developer-guide/strategies-configurable-operations/#injecting-dependencies). ::: ## Fulfillments Fulfillments represent the actual shipping status of items in an order. When an order is placed and payment has been settled, the order items are then delivered to the customer in one or more Fulfillments. * **Physical goods:** A fulfillment would represent the actual boxes or packages which are shipped to the customer. When the package leaves the warehouse, the fulfillment is marked as `Shipped`. When the package arrives with the customer, the fulfillment is marked as `Delivered`. * **Digital goods:** A fulfillment would represent the means of delivering the digital goods to the customer, e.g. a download link or a license key. For example, when the link is sent to the customer, the fulfillment can be marked as `Shipped` and then `Delivered`. ### FulfillmentHandlers It is often required to integrate your fulfillment process, e.g. with an external shipping API which provides shipping labels or tracking codes. This is done by defining [FulfillmentHandlers](/reference/typescript-api/fulfillment/fulfillment-handler/) (click the link for full documentation) and passing them in to the `shippingOptions.fulfillmentHandlers` array in your config. By default, Vendure uses a manual fulfillment handler, which requires the Administrator to manually enter the method and tracking code of the Fulfillment. ### Fulfillment state machine Like Orders, Fulfillments are governed by a [finite state machine](/reference/typescript-api/state-machine/fsm/) and by default, a Fulfillment can be in one of the [following states](/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate): * `Pending` The Fulfillment has been created * `Shipped` The Fulfillment has been shipped * `Delivered` The Fulfillment has arrived with the customer * `Cancelled` The Fulfillment has been cancelled These states cover the typical workflow for fulfilling orders. However, it is possible to customize the fulfillment workflow by defining a [FulfillmentProcess](/reference/typescript-api/fulfillment/fulfillment-process) and passing it to your VendureConfig: ```ts title="src/vendure-config.ts" import { FulfillmentProcess, VendureConfig } from '@vendure/core'; import { myCustomFulfillmentProcess } from './my-custom-fulfillment-process'; export const config: VendureConfig = { // ... shippingOptions: { process: [myCustomFulfillmentProcess], }, }; ``` :::info For a more detailed look at how custom processes are used, see the [custom order processes guide](/guides/core-concepts/orders/#custom-order-processes). ::: --- --- title: "Stock Control" --- import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Vendure includes features to help manage your stock levels, stock allocations and back orders. The basic purpose is to help you keep track of how many of a given ProductVariant you have available to sell. Stock control is enabled globally via the Global Settings:  It can be disabled if, for example, you manage your stock with a separate inventory management system and synchronize stock levels into Vendure automatically. The setting can also be overridden at the individual ProductVariant level. ## Stock Locations Vendure uses the concept of [`StockLocations`](/reference/typescript-api/entities/stock-location/) to represent the physical locations where stock is stored. This could be a warehouse, a retail store, or any other location. If you do not have multiple stock locations, then you can simply use the default location which is created automatically.  ### Selecting a stock location When you have multiple stock locations set up, you need a way to determine which location to use when querying stock levels and when allocating stock to orders. This is handled by the [`StockLocationStrategy`](/reference/typescript-api/products-stock/stock-location-strategy/). This strategy exposes a number of methods which are used to determine which location (or locations) to use when: - querying stock levels (`getAvailableStock`) - allocating stock to orders (`forAllocation`) - releasing stock from orders (`forRelease`) - creating sales upon fulfillment (`forSale`) - returning items to stock upon cancellation (`forCancellation`) The default strategy is the [`DefaultStockLocationStrategy`](/reference/typescript-api/products-stock/default-stock-location-strategy), which simply uses the default location for all of the above methods. This is suitable for all cases where there is just a single stock location. If you have multiple stock locations, you'll need to implement a custom strategy which uses custom logic to determine which stock location to use. For instance, you could: - Use the location with the most stock available - Use the location closest to the customer - Use the location which has the cheapest shipping cost ### Displaying stock levels in the storefront The [`StockDisplayStrategy`](/reference/typescript-api/products-stock/stock-display-strategy/) is used to determine how stock levels are displayed in the storefront. The default strategy is the [`DefaultStockDisplayStrategy`](/reference/typescript-api/products-stock/default-stock-display-strategy), which will only display one of three states: `'IN_STOCK'`, `'OUT_OF_STOCK'` or `'LOW_STOCK'`. This is to avoid exposing your exact stock levels to the public, which can sometimes be undesirable. You can implement a custom strategy to display stock levels in a different way. Here's how you would implement a custom strategy to display exact stock levels: ```ts title="src/exact-stock-display-strategy.ts" import { RequestContext, StockDisplayStrategy, ProductVariant } from '@vendure/core'; export class ExactStockDisplayStrategy implements StockDisplayStrategy { getStockLevel(ctx: RequestContext, productVariant: ProductVariant, saleableStockLevel: number): string { return saleableStockLevel.toString(); } } ``` This strategy is then used in your config: ```ts title="src/vendure-config.ts" import { VendureConfig } from '@vendure/core'; import { ExactStockDisplayStrategy } from './exact-stock-display-strategy'; export const config: VendureConfig = { // ... catalogOptions: { stockDisplayStrategy: new ExactStockDisplayStrategy(), }, }; ``` ## Stock Control Concepts * **Stock on hand:** This refers to the number of physical units of a particular variant which you have in stock right now. This can be zero or more, but not negative. * **Allocated:** This refers to the number of units which have been assigned to Orders, but which have not yet been fulfilled. * **Out-of-stock threshold:** This value determines the stock level at which the variant is considered "out of stock". This value is set globally, but can be overridden for specific variants. It defaults to `0`. * **Saleable:** This means the number of units that can be sold right now. The formula is: `saleable = stockOnHand - allocated - outOfStockThreshold` Here's a table to better illustrate the relationship between these concepts: Stock on hand | Allocated | Out-of-stock threshold | Saleable --------------|-----------|------------------------|---------- 10 | 0 | 0 | 10 10 | 0 | 3 | 7 10 | 5 | 0 | 5 10 | 5 | 3 | 2 10 | 10 | 0 | 0 10 | 10 | -5 | 5 The saleable value is what determines whether the customer is able to add a variant to an order. If there is 0 saleable stock, then any attempt to add to the order will result in an [`InsufficientStockError`](/reference/graphql-api/admin/object-types/#insufficientstockerror). ```graphql title="Shop API" query GetEligibleShippingMethods { eligibleShippingMethods { id name price priceWithTax } } ``` ```json { "data": { "eligibleShippingMethods": [ { "id": "1", "name": "Standard Shipping", "price": 500, "priceWithTax": 500 }, { "id": "2", "name": "Express Shipping", "price": 1000, "priceWithTax": 1000 } ] } } ``` ### Stock allocation Allocation mean we are setting stock aside because it has been purchased but not yet shipped. It prevents us from selling more of a particular item than we are able to deliver. By default, stock gets allocated to an order once the order transitions to the `PaymentAuthorized` or `PaymentSettled` state. This is defined by the [`DefaultStockAllocationStrategy`](/reference/typescript-api/orders/default-stock-allocation-strategy). Using a custom [`StockAllocationStrategy`](/reference/typescript-api/orders/stock-allocation-strategy/) you can define your own rules for when stock is allocated. With the [`defaultFulfillmentProcess`](/reference/typescript-api/fulfillment/fulfillment-process/#defaultfulfillmentprocess), allocated stock will be converted to **sales** and minused from the `stockOnHand` value when a Fulfillment is created. ### Back orders You may have noticed that the `outOfStockThreshold` value can be set to a negative number. This allows you to sell variants even when you don't physically have them in stock. This is known as a "back order". Back orders can be really useful to allow orders to keep flowing even when stockOnHand temporarily drops to zero. For many businesses with predictable re-supply schedules they make a lot of sense. Once a customer completes checkout, those variants in the order are marked as `allocated`. When a Fulfillment is created, those allocations are converted to Sales and the `stockOnHand` of each variant is adjusted. Fulfillments may only be created if there is sufficient stock on hand. ### Stock movements There is a [`StockMovement`](/reference/typescript-api/entities/stock-movement/) entity which records the history of stock changes. `StockMovement` is actually an abstract class, with the following concrete implementations: - [`Allocation`](/reference/typescript-api/entities/stock-movement/#allocation): When stock is allocated to an order, before the order is fulfilled. Adds stock to `allocated`, which reduces the saleable stock. - [`Sale`](/reference/typescript-api/entities/stock-movement/#sale): When allocated stock gets fulfilled. Removes stock from `allocated` as well as `stockOnHand`. - [`Cancellation`](/reference/typescript-api/entities/stock-movement/#cancellation): When items from a fulfilled order are cancelled, the stock is returned to `stockOnHand`. Adds stock to `stockOnHand`. - [`Release`](/reference/typescript-api/entities/stock-movement/#release): When items which have been allocated (but not yet converted to sales via the creation of a Fulfillment) are cancelled. Removes stock from `allocated`. - [`StockAdjustment`](/reference/typescript-api/entities/stock-movement/#stockadjustment): A general-purpose stock adjustment. Adds or removes stock from `stockOnHand`. Used when manually setting stock levels via the Admin UI, for example. Stock movements can be queried via the `ProductVariant.stockMovements`. Here's an example where we query the stock levels and stock movements of a particular variant: ```graphql title="Shop API" query AddItemToOrder { addItemToOrder(productVariantId: 123, quantity: 150) { ...on Order { id code totalQuantity } ...on ErrorResult { errorCode message } ...on InsufficientStockError { errorCode message quantityAvailable order { id totalQuantity } } } } ``` ```json { "data": { "addItemToOrder": { "errorCode": "INSUFFICIENT_STOCK_ERROR", "message": "Only 105 items were added to the order due to insufficient stock", "quantityAvailable": 105, "order": { "id": "2", "totalQuantity": 106 } } } } ``` --- --- title: "Taxes" showtoc: true --- E-commerce applications need to correctly handle taxes such as sales tax or value added tax (VAT). In Vendure, tax handling consists of: * **Tax categories** Each ProductVariant is assigned to a specific TaxCategory. In some tax systems, the tax rate differs depending on the type of good. For example, VAT in the UK has 3 rates, "standard" (most goods), "reduced" (e.g. child car seats) and "zero" (e.g. books). * **Tax rates** This is the tax rate applied to a specific tax category for a specific [Zone](/reference/typescript-api/entities/zone/). E.g., the tax rate for "standard" goods in the UK Zone is 20%. * **Channel tax settings** Each Channel can specify whether the prices of product variants are inclusive of tax or not, and also specify the default Zone to use for tax calculations. * **TaxZoneStrategy** Determines the active tax Zone used when calculating what TaxRate to apply. By default, it uses the default tax Zone from the Channel settings. * **TaxLineCalculationStrategy** This determines the taxes applied when adding an item to an Order. If you want to integrate a 3rd-party tax API or other async lookup, this is where it would be done. ## API conventions In the GraphQL API, any type which has a taxable price will split that price into two fields: `price` and `priceWithTax`. This pattern also holds for other price fields, e.g. ```graphql query { activeOrder { ...on Order { lines { linePrice linePriceWithTax } subTotal subTotalWithTax shipping shippingWithTax total totalWithTax } } } ``` In your storefront, you can therefore choose whether to display the prices with or without tax, according to the laws and conventions of the area in which your business operates. ## Calculating tax on order lines When a customer adds an item to the Order, the following logic takes place: 1. The price of the item, and whether that price is inclusive of tax, is determined according to the configured [OrderItemPriceCalculationStrategy](/reference/typescript-api/orders/order-item-price-calculation-strategy/). 2. The active tax Zone is determined based on the configured [TaxZoneStrategy](/reference/typescript-api/tax/tax-zone-strategy/). By default, Vendure will use the default tax Zone from the Channel settings. However, you often want to use the customer's address as the basis for determining the tax Zone. In this case, you should use the [AddressBasedTaxZoneStrategy](/reference/typescript-api/tax/address-based-tax-zone-strategy). 3. The applicable TaxRate is fetched based on the ProductVariant's TaxCategory and the active tax Zone determined in step 1. 4. The `TaxLineCalculationStrategy.calculate()` of the configured [TaxLineCalculationStrategy](/reference/typescript-api/tax/tax-line-calculation-strategy/) is called, which will return one or more [TaxLines](/reference/graphql-api/admin/object-types/#taxline). 5. The final `priceWithTax` of the order line is calculated based on all the above. ## Calculating tax on shipping The taxes on shipping is calculated by the [ShippingCalculator](/reference/typescript-api/shipping/shipping-calculator/) of the Order's selected [ShippingMethod](/reference/typescript-api/entities/shipping-method/). ## Configuration This example shows the default configuration for taxes (you don't need to specify this in your own config, as these are the defaults): ```ts title="src/vendure-config.ts" import { DefaultTaxLineCalculationStrategy, DefaultTaxZoneStrategy, DefaultOrderItemPriceCalculationStrategy, VendureConfig } from '@vendure/core'; export const config: VendureConfig = { taxOptions: { taxZoneStrategy: new DefaultTaxZoneStrategy(), taxLineCalculationStrategy: new DefaultTaxLineCalculationStrategy(), }, orderOptions: { orderItemPriceCalculationStrategy: new DefaultOrderItemPriceCalculationStrategy() } } ``` --- --- title: "Deploying to Digital Ocean" ---  [App Platform](https://www.digitalocean.com/products/app-platform) is a fully managed platform which allows you to deploy and scale your Vendure server and infrastructure with ease. :::note The configuration in this guide will cost around $22 per month to run. ::: ## Prerequisites First of all you'll need to [create a new Digital Ocean account](https://cloud.digitalocean.com/registrations/new) if you don't already have one. For this guide you'll need to have your Vendure project in a git repo on either GitHub or GitLab. App Platform also supports deploying from docker registries, but that is out of the scope of this guide. :::info If you'd like to quickly get started with a ready-made Vendure project which includes sample data, you can clone our [Vendure one-click-deploy repo](https://github.com/vendure-ecommerce/one-click-deploy). ::: ## Configuration ### Database connection :::info The following is already pre-configured if you are using the one-click-deploy repo. ::: Make sure your DB connection options uses the following environment variables: ```ts title="src/vendure-config.ts" import { VendureConfig } from '@vendure/core'; export const config: VendureConfig = { // ... dbConnectionOptions: { // ... type: 'postgres', database: process.env.DB_NAME, host: process.env.DB_HOST, port: +process.env.DB_PORT, username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, ssl: process.env.DB_CA_CERT ? { ca: process.env.DB_CA_CERT, } : undefined, }, }; ``` ### Asset storage :::info The following is already pre-configured if you are using the one-click-deploy repo. ::: Since App Platform services do not include any persistent storage, we need to configure Vendure to use Digital Ocean's Spaces service, which is an S3-compatible object storage service. This means you'll need to make sure to have the following packages installed: ``` npm install @aws-sdk/client-s3 @aws-sdk/lib-storage ``` and set up your AssetServerPlugin like this: ```ts title="src/vendure-config.ts" import { VendureConfig } from '@vendure/core'; import { AssetServerPlugin, configureS3AssetStorage } from '@vendure/asset-server-plugin'; export const config: VendureConfig = { // ... plugins: [ AssetServerPlugin.init({ route: 'assets', assetUploadDir: process.env.ASSET_UPLOAD_DIR || path.join(__dirname, '../static/assets'), // highlight-start // If the MINIO_ENDPOINT environment variable is set, we'll use // Minio as the asset storage provider. Otherwise, we'll use the // default local provider. storageStrategyFactory: process.env.MINIO_ENDPOINT ? configureS3AssetStorage({ bucket: 'vendure-assets', credentials: { accessKeyId: process.env.MINIO_ACCESS_KEY, secretAccessKey: process.env.MINIO_SECRET_KEY, }, nativeS3Configuration: { endpoint: process.env.MINIO_ENDPOINT, forcePathStyle: true, signatureVersion: 'v4', // The `region` is required by the AWS SDK even when using MinIO, // so we just use a dummy value here. region: 'eu-west-1', }, }) : undefined, // highlight-end }), ], // ... }; ``` ## Create Spaces Object Storage First we'll create a Spaces bucket to store our assets. Click the "Spaces Object Storage" nav item and create a new space and call it "vendure-assets".  Next we need to create an access key and secret. Click the "API" nav item and generate a new key.  Name the key something meaningful like "vendure-assets-key" and then **make sure to copy the secret as it will only be shown once**. Store the access key and secret key in a safe place for later - we'll be using it when we set up our app's environment variables. :::caution If you forget to copy the secret key, you'll need to delete the key and create a new one. ::: ## Create the server resource Now we're ready to create our app infrastructure! Click the "Create" button in the top bar and select "Apps".  Now connect to your git repo, and select the repo of your Vendure project. Depending on your repo, App Platform may suggest more than one app: in this screenshot we are using the one-click-deploy repo which contains a Dockerfile, so App Platform is suggesting two different ways to deploy the app. We'll select the Dockerfile option, but either option should work fine. Delete the unused resource. We need to edit the details of the server app. Click the "Edit" button and set the following: * **Resource Name**: "vendure-server" * **Resource Type**: Web Service * **Run Command**: `node ./dist/index.js` * **HTTP Port**: 3000 At this point you can also click the "Edit Plan" button to select the resource allocation for the server, which will determine performance and price. For testing purposes the smallest Basic server (512MB, 1vCPU) is fine. This can also be changed later. ### Add a database Next click "Add Resource", select **Database** and click "Add", and then accept the default Postgres database. Click the "Create and Attach" to create the database and attach it to the server app. Your config should now look like this:  ## Set up environment variables Next we need to set up the environment variables. Since these will be shared by both the server and worker apps, we'll create them at the Global level. You can use the "bulk editor" to paste in the following variables (making sure to replace the values in ` ```graphql title="Admin API" query GetStockMovements { productVariant(id: 1) { id name stockLevels { stockLocation { name } stockOnHand stockAllocated } stockMovements { items { ...on StockMovement { createdAt type quantity } } } } } ``` ```json { "data": { "productVariant": { "id": "1", "name": "Laptop 13 inch 8GB", "stockLevels": [ { "stockLocation": { "name": "Default Stock Location" }, "stockOnHand": 100, "stockAllocated": 0 } ], "stockMovements": { "items": [ { "createdAt": "2023-07-13T13:21:10.000Z", "type": "ADJUSTMENT", "quantity": 100 } ] } } } } ``` ` with your own values): ```sh DB_NAME=${db.DATABASE} DB_USERNAME=${db.USERNAME} DB_PASSWORD=${db.PASSWORD} DB_HOST=${db.HOSTNAME} DB_PORT=${db.PORT} DB_CA_CERT=${db.CA_CERT} // highlight-next-line COOKIE_SECRET= SUPERADMIN_USERNAME=superadmin // highlight-next-line SUPERADMIN_PASSWORD= // highlight-start MINIO_ACCESS_KEY=