Orders
In Vendure, the 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
is composed of one or more OrderLines
.
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
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 (a method of precisely controlling how the order moves from one state to another). This means that the Order.state
property will be one of a set of pre-defined states. 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.
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:
- Request
- Response
query ActiveOrder {
activeOrder {
id
state
}
}
{
"data": {
"activeOrder": {
"id": "4",
"state": "AddingItems"
}
}
}
The next possible states can be queried via the nextOrderStates
query:
- Request
- Response
query NextStates {
nextOrderStates
}
{
"data": {
"nextOrderStates": [
"ArrangingPayment",
"Cancelled"
]
}
}
The available states and the permissible transitions between them are defined by the configured OrderProcess
. By default, Vendure defines a 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 theAddingItems
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 theArrangingPayment
, 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 theaddPaymentToOrder
mutation.PaymentAuthorized:
Depending on the configured payment method, the order may then transition to thePaymentAuthorized
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 thePaymentSettled
state.PaymentSettled:
If the payment captured immediately, the order will transition to thePaymentSettled
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 theaddFulfillmentToOrder
mutation, or via the Admin UI. If multiple fulfillments are created, then the order can end up partial states -PartiallyShipped
orPartiallyDelivered
. 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 theShipped
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 theDelivered
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 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 function, and then passing it to the OrderOptions.process
config property.
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
objects and passing them to the OrderOptions.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 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:
AddingItems -> ArrangingPayment
to instead be:
AddingItems -> ValidatingCustomer -> ArrangingPayment
Here's how we would define the new state:
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 theValidatingCustomer
state (mergeStrategy: 'replace'
tells Vendure to discard any existing transition targets and replace with this one). - the
ValidatingCustomer
may transition to theArrangingPayment
state (assuming the tax ID is valid) or back to theAddingItems
state.
And then add this configuration to our main VendureConfig:
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:
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.
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.
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`;
}
}
},
};
For an explanation of the init()
method and injector
argument, see the guide on injecting dependencies in configurable operations.
Responding to a state transition
Once an order has successfully transitioned to a new state, the onTransitionEnd
state transition hook 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.
import { OrderProcess, OrderState } from '@vendure/core';
import { ReferralService } from '../service/referral.service';
let referralService: ReferralService;
export const referralOrderProcess: OrderProcess<OrderState> = {
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);
}
}
},
};
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:
export const myOrderProcess: OrderProcess<OrderState> = {
async onTransitionEnd(fromState, toState, data) {
if (fromState === 'AddingItems' && toState === 'ArrangingPayment') {
// WARNING: This will not work!
await orderService.addSurchargeToOrder(ctx, order.id, {
description: 'Test',
listPrice: 42,
listPriceIncludesTax: false,
});
}
}
};
Instead, you need to ensure you mutate the order
object:
export const myOrderProcess: OrderProcess<OrderState> = {
async onTransitionEnd(fromState, toState, data) {
if (fromState === 'AddingItems' && toState === 'ArrangingPayment') {
const {surcharges} = await orderService.addSurchargeToOrder(ctx, order.id, {
description: 'Test',
listPrice: 42,
listPriceIncludesTax: false,
});
// Important: mutate the order object
order.surcharges = surcharges;
}
},
}
TypeScript Typings
To make your custom states compatible with standard services you should declare your new states in the following way:
import { CustomOrderStates } from '@vendure/core';
declare module '@vendure/core' {
interface CustomOrderStates {
ValidatingCustomer: never;
}
}
This technique uses advanced TypeScript features - declaration merging and 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. 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 docs for more information as well as a complete example of how to implement an interceptor.