Browse Source

docs: Update core concepts guides

Michael Bromley 2 years ago
parent
commit
c82e30baea
31 changed files with 2056 additions and 604 deletions
  1. 0 0
      docs/docs/guides/advanced-topics/stand-alone-scripts.md
  2. 0 0
      docs/docs/guides/advanced-topics/testing.md
  3. 0 0
      docs/docs/guides/advanced-topics/translations.md
  4. 0 0
      docs/docs/guides/advanced-topics/updating-vendure.md
  5. 468 0
      docs/docs/guides/core-concepts/authentication/index.md
  6. BIN
      docs/docs/guides/core-concepts/channels/channel-token.webp
  7. BIN
      docs/docs/guides/core-concepts/channels/channels_currencies_diagram.png
  8. BIN
      docs/docs/guides/core-concepts/channels/channels_diagram.png
  9. BIN
      docs/docs/guides/core-concepts/channels/channels_prices_diagram.png
  10. BIN
      docs/docs/guides/core-concepts/channels/default-currency.webp
  11. 122 0
      docs/docs/guides/core-concepts/channels/index.md
  12. BIN
      docs/docs/guides/core-concepts/channels/variant-prices.webp
  13. BIN
      docs/docs/guides/core-concepts/orders/custom-order-ui.webp
  14. 260 0
      docs/docs/guides/core-concepts/orders/index.md
  15. BIN
      docs/docs/guides/core-concepts/orders/order-process.webp
  16. 411 0
      docs/docs/guides/core-concepts/payment/index.md
  17. BIN
      docs/docs/guides/core-concepts/payment/payment-method.webp
  18. BIN
      docs/docs/guides/core-concepts/payment/payment_sequence_one_step.png
  19. BIN
      docs/docs/guides/core-concepts/payment/payment_sequence_two_step.png
  20. 315 0
      docs/docs/guides/core-concepts/promotions/index.md
  21. 225 0
      docs/docs/guides/core-concepts/shipping/index.md
  22. BIN
      docs/docs/guides/core-concepts/shipping/shipping-method.webp
  23. BIN
      docs/docs/guides/core-concepts/stock-control/global-stock-control.webp
  24. 233 0
      docs/docs/guides/core-concepts/stock-control/index.md
  25. BIN
      docs/docs/guides/core-concepts/stock-control/stock-levels.webp
  26. 18 12
      docs/docs/guides/core-concepts/taxes/index.mdx
  27. 0 314
      docs/docs/guides/developer-guide-old/promotions.md
  28. 0 165
      docs/docs/guides/developer-guide-old/shipping.md
  29. 0 94
      docs/docs/guides/developer-guide-old/vendure-worker.md
  30. 4 19
      docs/docs/guides/developer-guide/data-model/index.mdx
  31. 0 0
      docs/docs/guides/how-to/uploading-files.md

+ 0 - 0
docs/docs/guides/developer-guide-old/stand-alone-scripts.md → docs/docs/guides/advanced-topics/stand-alone-scripts.md


+ 0 - 0
docs/docs/guides/developer-guide-old/testing.md → docs/docs/guides/advanced-topics/testing.md


+ 0 - 0
docs/docs/guides/developer-guide-old/translations.md → docs/docs/guides/advanced-topics/translations.md


+ 0 - 0
docs/docs/guides/developer-guide-old/updating-vendure.md → docs/docs/guides/advanced-topics/updating-vendure.md


+ 468 - 0
docs/docs/guides/core-concepts/authentication/index.md

@@ -0,0 +1,468 @@
+---
+title: 'Authentication'
+---
+
+Authentication is the process of determining the identity of a user. Common ways of authenticating a user are by asking the user for secret credentials (username & password) or by a third-party authentication provider such as Facebook or Google login.
+
+By default, Vendure uses a username/email address and password to authenticate users, but also supports a wide range of authentication methods via configurable AuthenticationStrategies.
+
+:::info
+See the [Managing Sessions guide](/TODO) for how to manage authenticated sessions in your storefront/client applications.
+:::
+
+## Adding support for external authentication
+
+This is done via the [`VendureConfig.authOptions` object](/reference/typescript-api/auth/auth-options/#shopauthenticationstrategy):
+
+```ts title="vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { NativeAuthenticationStrategy } from './plugins/authentication/native-authentication-strategy';
+import { FacebookAuthenticationStrategy } from './plugins/authentication/facebook-authentication-strategy';
+import { GoogleAuthenticationStrategy } from './plugins/authentication/google-authentication-strategy';
+import { KeycloakAuthenticationStrategy } from './plugins/authentication/keycloak-authentication-strategy';
+
+export const config: VendureConfig = {
+  authOptions: {
+      shopAuthenticationStrategy: [
+        new NativeAuthenticationStrategy(),
+        new FacebookAuthenticationStrategy(),
+        new GoogleAuthenticationStrategy(),
+      ],
+      adminAuthenticationStrategy: [
+        new NativeAuthenticationStrategy(),
+        new KeycloakAuthenticationStrategy(),
+      ],
+  }
+}
+```
+
+In the above example, we define the strategies available for authenticating in the Shop API and the Admin API. The `NativeAuthenticationStrategy` is the only one actually provided by Vendure out-of-the-box, and this is the default username/email + password strategy.
+
+The other strategies would be custom-built (or provided by future npm packages) by creating classes that implement the [`AuthenticationStrategy` interface](/reference/typescript-api/auth/authentication-strategy).
+
+Let's take a look at a couple of examples of what a custom AuthenticationStrategy implementation would look like.
+
+## Example: Google authentication
+
+This example demonstrates how to implement a Google login flow.
+
+### Storefront setup
+
+In your storefront, you need to integrate the Google sign-in button as described in ["Integrating Google Sign-In into your web app"](https://developers.google.com/identity/sign-in/web/sign-in). Successful authentication will result in a `onSignIn` function being called in your app. It will look something like this:
+
+```ts
+function onSignIn(googleUser) {
+  graphQlQuery(
+    `mutation Authenticate($token: String!) {
+        authenticate(input: {
+          google: { token: $token }
+        }) {
+        ...on CurrentUser {
+            id
+            identifier
+        }
+      }
+    }`,
+    { token: googleUser.getAuthResponse().id_token }
+  ).then(() => {
+    // redirect to account page
+  });
+}
+```
+
+### Backend
+
+On the backend, you'll need to define an AuthenticationStrategy to take the authorization token provided by the
+storefront in the `authenticate` mutation, and use it to get the necessary personal information on that user from
+Google.
+
+To do this you'll need to install the `google-auth-library` npm package as described in the ["Authenticate with a backend server" guide](https://developers.google.com/identity/sign-in/web/backend-auth).
+
+```ts title="src/plugins/authentication/google-authentication-strategy.ts"
+import {
+    AuthenticationStrategy,
+    ExternalAuthenticationService,
+    Injector,
+    RequestContext,
+    User,
+} from '@vendure/core';
+import { OAuth2Client } from 'google-auth-library';
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+export type GoogleAuthData = {
+    token: string;
+};
+
+export class GoogleAuthenticationStrategy implements AuthenticationStrategy<GoogleAuthData> {
+    readonly name = 'google';
+    private client: OAuth2Client;
+    private externalAuthenticationService: ExternalAuthenticationService;
+
+    constructor(private clientId: string) {
+        // The clientId is obtained by creating a new OAuth client ID as described
+        // in the Google guide linked above.
+        this.client = new OAuth2Client(clientId);
+    }
+
+    init(injector: Injector) {
+        // The ExternalAuthenticationService is a helper service which encapsulates much
+        // of the common functionality related to dealing with external authentication
+        // providers.
+        this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+    }
+
+    defineInputType(): DocumentNode {
+        // Here we define the expected input object expected by the `authenticate` mutation
+        // under the "google" key.
+        return gql`
+        input GoogleAuthInput {
+            token: String!
+        }
+    `;
+    }
+
+    async authenticate(ctx: RequestContext, data: GoogleAuthData): Promise<User | false> {
+        // Here is the logic that uses the token provided by the storefront and uses it
+        // to find the user data from Google.
+        const ticket = await this.client.verifyIdToken({
+            idToken: data.token,
+            audience: this.clientId,
+        });
+        const payload = ticket.getPayload();
+        if (!payload || !payload.email) {
+            return false;
+        }
+
+        // First we check to see if this user has already authenticated in our
+        // Vendure server using this Google account. If so, we return that
+        // User object, and they will be now authenticated in Vendure.
+        const user = await this.externalAuthenticationService.findCustomerUser(ctx, this.name, payload.sub);
+        if (user) {
+            return user;
+        }
+
+        // If no user was found, we need to create a new User and Customer based
+        // on the details provided by Google. The ExternalAuthenticationService
+        // provides a convenience method which encapsulates all of this into
+        // a single method call.
+        return this.externalAuthenticationService.createCustomerAndUser(ctx, {
+            strategy: this.name,
+            externalIdentifier: payload.sub,
+            verified: payload.email_verified || false,
+            emailAddress: payload.email,
+            firstName: payload.given_name,
+            lastName: payload.family_name,
+        });
+    }
+}
+```
+
+## Example: Facebook authentication
+
+This example demonstrates how to implement a Facebook login flow.
+
+### Storefront setup
+
+In this example, we are assuming the use of the [Facebook SDK for JavaScript](https://developers.facebook.com/docs/javascript/) in the storefront.
+
+An implementation in React might look like this:
+
+```tsx title="/storefront/src/components/FacebookLoginButton.tsx"
+/**
+ * Renders a Facebook login button.
+ */
+export const FBLoginButton = () => {
+    const fnName = `onFbLoginButtonSuccess`;
+    const router = useRouter();
+    const [error, setError] = useState('');
+    const [socialLoginMutation] = useMutation(AuthenticateDocument);
+
+    useEffect(() => {
+        (window as any)[fnName] = function() {
+            FB.getLoginStatus(login);
+        };
+        return () => {
+            delete (window as any)[fnName];
+        };
+    }, []);
+
+    useEffect(() => {
+        window?.FB?.XFBML.parse();
+    }, []);
+
+    const login = async (response: any) => {
+        const {status, authResponse} = response;
+        if (status === 'connected') {
+            const result = await socialLoginMutation({variables: {token: authResponse.accessToken}});
+            if (result.data?.authenticate.__typename === 'CurrentUser') {
+                // The user has logged in, refresh the browser
+                trackLogin('facebook');
+                router.reload();
+                return;
+            }
+        }
+        setError('An error occurred!');
+    };
+
+    return (
+        <div className="text-center" style={{ width: 188, height: 28 }}>
+            <FacebookSDK />
+            <div
+                className="fb-login-button"
+                data-width=""
+                data-size="medium"
+                data-button-type="login_with"
+                data-layout="default"
+                data-auto-logout-link="false"
+                data-use-continue-as="false"
+                data-scope="public_profile,email"
+                data-onlogin={`${fnName}();`}
+            />
+            {error && <div className="text-sm text-red-500">{error}</div>}
+        </div>
+  );
+};
+```
+
+```ts title="/src/plugins/authentication/facebook-authentication-strategy.ts"
+import {
+    AuthenticationStrategy,
+    ExternalAuthenticationService,
+    Injector,
+    Logger,
+    RequestContext,
+    User,
+    UserService,
+} from '@vendure/core';
+
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+import fetch from 'node-fetch';
+
+export type FacebookAuthData = {
+    token: string;
+};
+
+export type FacebookAuthConfig = {
+    appId: string;
+    appSecret: string;
+    clientToken: string;
+};
+
+export class FacebookAuthenticationStrategy implements AuthenticationStrategy<FacebookAuthData> {
+    readonly name = 'facebook';
+    private externalAuthenticationService: ExternalAuthenticationService;
+    private userService: UserService;
+
+    constructor(private config: FacebookAuthConfig) {
+    }
+
+    init(injector: Injector) {
+        // The ExternalAuthenticationService is a helper service which encapsulates much
+        // of the common functionality related to dealing with external authentication
+        // providers.
+        this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+        this.userService = injector.get(UserService);
+    }
+
+    defineInputType(): DocumentNode {
+        // Here we define the expected input object expected by the `authenticate` mutation
+        // under the "google" key.
+        return gql`
+      input FacebookAuthInput {
+        token: String!
+      }
+    `;
+    }
+
+    private async getAppAccessToken() {
+        const resp = await fetch(
+            `https://graph.facebook.com/oauth/access_token?client_id=${this.config.appId}&client_secret=${this.config.appSecret}&grant_type=client_credentials`,
+        );
+        return await resp.json();
+    }
+
+    async authenticate(ctx: RequestContext, data: FacebookAuthData): Promise<User | false> {
+        const {token} = data;
+        const {access_token} = await this.getAppAccessToken();
+        const resp = await fetch(
+            `https://graph.facebook.com/debug_token?input_token=${token}&access_token=${access_token}`,
+        );
+        const result = await resp.json();
+
+        if (!result.data) {
+            return false;
+        }
+
+        const uresp = await fetch(`https://graph.facebook.com/me?access_token=${token}&fields=email,first_name,last_name`);
+        const uresult = (await uresp.json()) as { id?: string; email: string; first_name: string; last_name: string };
+
+        if (!uresult.id) {
+            return false;
+        }
+
+        const existingUser = await this.externalAuthenticationService.findCustomerUser(ctx, this.name, uresult.id);
+
+        if (existingUser) {
+            // This will select all the auth methods
+            return (await this.userService.getUserById(ctx, existingUser.id))!;
+        }
+
+        Logger.info(`User Create: ${JSON.stringify(uresult)}`);
+        const user = await this.externalAuthenticationService.createCustomerAndUser(ctx, {
+            strategy: this.name,
+            externalIdentifier: uresult.id,
+            verified: true,
+            emailAddress: uresult.email,
+            firstName: uresult.first_name,
+            lastName: uresult.last_name,
+        });
+
+        user.verified = true;
+        return user;
+    }
+}
+```
+
+## Example: Keycloak authentication
+
+Here's an example of an AuthenticationStrategy intended to be used on the Admin API. The use-case is when the company has an existing identity server for employees, and you'd like your Vendure shop admins to be able to authenticate with their existing accounts.
+
+This example uses [Keycloak](https://www.keycloak.org/), a popular open-source identity management server. To get your own Keycloak server up and running in minutes, follow the [Keycloak on Docker](https://www.keycloak.org/getting-started/getting-started-docker) guide.
+
+### Configure a login page & Admin UI
+
+In this example, we'll assume the login page is hosted at `http://intranet/login`. We'll also assume that a "login to Vendure" button has been added to that page and that the page is using the [Keycloak JavaScript adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter), which can be used to get the current user's authorization token:
+
+```js title="/login/index.html"
+const vendureLoginButton = document.querySelector('#vendure-login-button');
+
+vendureLoginButton.addEventListener('click', () => {
+  return graphQlQuery(`
+    mutation Authenticate($token: String!) {
+      authenticate(input: {
+        keycloak: {
+          token: $token
+        }
+      }) {
+        ...on CurrentUser { id }
+      }
+    }`,
+    { token: keycloak.token },
+  )
+  .then((result) => {
+      if (result.data?.authenticate.user) {
+          // successfully authenticated - redirect to Vendure Admin UI
+          window.location.replace('http://localhost:3000/admin');
+      }
+  });
+});
+```
+
+We also need to tell the Admin UI application about the custom login URL, since we have no need for the default "username/password" login form. This can be done by setting the [`loginUrl` property](/reference/typescript-api/core-plugins/admin-ui-plugin/admin-ui-config/#loginurl) in the AdminUiConfig:
+
+```ts title="/src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AdminUiPlugin.init({
+            port: 5001,
+            adminUiConfig: {
+                loginUrl: 'http://intranet/login',
+            },
+        }),
+    ],
+};
+```
+
+### Backend
+
+First we will need to be making an HTTP call to our Keycloak server to validate the token and get the user's details. We'll use the [`node-fetch`](https://www.npmjs.com/package/node-fetch) library to make the HTTP call:
+
+```bash
+npm install node-fetch
+```
+
+The strategy is very similar to the Google authentication example (they both use the OpenID Connect standard), so we'll not duplicate the explanatory comments here:
+
+```ts title="/src/plugins/authentication/keycloak-authentication-strategy.ts"
+import fetch from 'node-fetch';
+import {
+    AuthenticationStrategy,
+    ExternalAuthenticationService,
+    Injector,
+    Logger,
+    RequestContext,
+    RoleService,
+    User,
+} from '@vendure/core';
+import { DocumentNode } from 'graphql';
+import gql from 'graphql-tag';
+
+export type KeycloakAuthData = {
+    token: string;
+};
+
+export class KeycloakAuthenticationStrategy implements AuthenticationStrategy<KeycloakAuthData> {
+    readonly name = 'keycloak';
+    private externalAuthenticationService: ExternalAuthenticationService;
+    private httpService: HttpService;
+    private roleService: RoleService;
+
+    init(injector: Injector) {
+        this.externalAuthenticationService = injector.get(ExternalAuthenticationService);
+        this.httpService = injector.get(HttpService);
+        this.roleService = injector.get(RoleService);
+    }
+
+    defineInputType(): DocumentNode {
+        return gql`
+      input KeycloakAuthInput {
+        token: String!
+      }
+    `;
+    }
+
+    async authenticate(ctx: RequestContext, data: KeycloakAuthData): Promise<User | false> {
+        const { data: userInfo } = await fetch(
+            'http://localhost:9000/auth/realms/myrealm/protocol/openid-connect/userinfo', {
+                headers: {
+                    Authorization: `Bearer ${data.token}`,
+                },
+            }).then(res => res.json());
+
+        if (!userInfo) {
+            return false;
+        }
+        const user = await this.externalAuthenticationService.findAdministratorUser(ctx, this.name, userInfo.sub);
+        if (user) {
+            return user;
+        }
+
+        // When creating an Administrator, we need to know what Role(s) to assign.
+        // In this example, we've created a "merchant" role and assign that to all
+        // new Administrators. In a real implementation, you can have more complex
+        // logic to map an external user to a given role.
+        const roles = await this.roleService.findAll();
+        const merchantRole = roles.items.find((r) => r.code === 'merchant');
+        if (!merchantRole) {
+            Logger.error(`Could not find "merchant" role`);
+            return false;
+        }
+
+        return this.externalAuthenticationService.createAdministratorAndUser(ctx, {
+            strategy: this.name,
+            externalIdentifier: userInfo.sub,
+            identifier: userInfo.preferred_username,
+            emailAddress: userInfo.email,
+            firstName: userInfo.given_name,
+            lastName: userInfo.family_name,
+            roles: [merchantRole],
+        });
+    }
+}
+```

BIN
docs/docs/guides/core-concepts/channels/channel-token.webp


BIN
docs/docs/guides/core-concepts/channels/channels_currencies_diagram.png


BIN
docs/docs/guides/core-concepts/channels/channels_diagram.png


BIN
docs/docs/guides/core-concepts/channels/channels_prices_diagram.png


BIN
docs/docs/guides/core-concepts/channels/default-currency.webp


+ 122 - 0
docs/docs/guides/core-concepts/channels/index.md

@@ -0,0 +1,122 @@
+---
+title: "Channels"
+---
+
+Channels are a feature of Vendure which allows multiple sales channels to be represented in a single Vendure instance. A Channel allows you to:
+
+* Set Channel-specific currency, language, tax and shipping defaults
+* Assign only specific products to the channel (with channel-specific prices)
+* Create administrator roles limited to one or more channels
+* Assign specific stock locations, assets, facets, collections, promotions, and other entities to the channel
+* Have orders and customers associated with specific channels.
+
+This is useful for a number of use-cases, including:
+
+- **Multi-tenancy**: Each channel can be configured with its own set of products, shipping methods, payment methods, etc. This
+  allows you to run multiple shops from a single Vendure server.
+- **Multi-vendor**: Each channel can represent a distinct vendor or seller, which can be used to implement a marketplace.
+- **Region-specific stores**: Each channel can be configured with its own set of languages, currencies, tax rates, etc. This
+  allows you to run multiple stores for different regions from a single Vendure server.
+- **Distinct sales channels**: Each channel can represent a sales channel of a single business, with one channel for the online
+  store, one for selling via Amazon, one for selling via Facebook etc.
+
+
+Every Vendure server always has a **default Channel**, which contains _all_ entities. Subsequent channels can then contain a subset of channel-aware entities.
+
+![Channels high level](../../developer-guide/data-model/channels.webp)
+
+## Channel-aware entities
+
+Many entities are channel-aware, meaning that they can be associated with a multiple channels. The following entities are channel-aware:
+
+- [`Asset`](/reference/typescript-api/entities/asset/)
+- [`Collection`](/reference/typescript-api/entities/collection/)
+- [`Customer`](/reference/typescript-api/entities/customer-group/)
+- [`Facet`](/reference/typescript-api/entities/facet/)
+- [`FacetValue`](/reference/typescript-api/entities/facet-value/)
+- [`Order`](/reference/typescript-api/entities/order/)
+- [`PaymentMethod`](/reference/typescript-api/entities/payment-method/)
+- [`Product`](/reference/typescript-api/entities/product/)
+- [`ProductVariant`](/reference/typescript-api/entities/product-variant/)
+- [`Promotion`](/reference/typescript-api/entities/promotion/)
+- [`Role`](/reference/typescript-api/entities/role/)
+- [`ShippingMethod`](/reference/typescript-api/entities/shipping-method/)
+- [`StockLocation`](/reference/typescript-api/entities/stock-location/)
+
+## Channels & Sellers
+
+Each channel is also assigned a single [`Seller`](/reference/typescript-api/entities/seller/). This entity is used to represent
+the vendor or seller of the products in the channel. This is useful for implementing a marketplace, where each channel represents
+a distinct vendor. The `Seller` entity can be extended with [custom fields](/guides/developer-guide/custom-fields/) to store additional information about the seller, such as a logo, contact details etc.
+
+## Channels, Currencies & Prices
+
+Each Channel has a set of `availableCurrencyCodes`, and one of these is designated as the `defaultCurrencyCode`, which sets the default currency for all monetary values in that channel.
+
+![Default currencies](./default-currency.webp)
+
+Internally, there is a one-to-many relation from [`ProductVariant`](/reference/typescript-api/entities/product-variant/) to [`ProductVariantPrice`](/reference/typescript-api/entities/product-variant-price). So the ProductVariant does _not_ hold a price for the product - this is actually stored on the `ProductVariantPrice` entity, and there will be at least one for each Channel to which the ProductVariant has been assigned.
+
+![Product variant prices](./variant-prices.webp)
+
+In this diagram we can see that every channel has at least 1 `ProductVariantPrice`. In the case of the UK Channel, there are 2 prices assigned - one for
+GBP and one for USD. This means that you are able to define multiple prices in different currencies on a single product variant for a single channel.
+
+:::info
+**Note:** in the diagram above that the ProductVariant is **always assigned to the default Channel**, and thus will have a price in the default channel too. Likewise, the default Channel also has a defaultCurrencyCode. Depending on your requirements, you may or may not make use of the default Channel.
+:::
+
+## Use cases
+
+### Single shop
+
+This is the simplest set-up. You just use the default Channel for everything.
+
+### Multiple separate shops
+
+Let's say you are running multiple distinct businesses, each with its own distinct inventory and possibly different currencies. In this case, you set up a Channel for each shop and create the Product & Variants in the relevant shop's Channel.
+
+The default Channel can then be used by the superadmin for administrative purposes, but other than that the default Channel would not be used. Storefronts would only target a specific shop's Channel.
+
+### Multiple shops sharing inventory
+
+Let's say you have a single inventory but want to split it between multiple shops. There might be overlap in the inventory, e.g. the US & EU shops share 80% of inventory, and then the rest is specific to either shop.
+
+In this case, you can create the entire inventory in the default Channel and then assign the Products & ProductVariants to each Channel as needed, setting the price as appropriate for the currency used by each shop.
+
+:::caution
+**Note:** When creating a new Product & ProductVariants inside a sub-Channel, it will also **always get assigned to the default Channel**. If your sub-Channel uses a different currency from the default Channel, you should be aware that in the default Channel, that ProductVariant will be assigned the **same price** as it has in the sub-Channel. If the currency differs between the Channels, you need to make sure to set the correct price in the default Channel if you are exposing it to Customers via a storefront. 
+:::
+
+### Multi-vendor marketplace
+
+This is the most advanced use of channels. For a detailed guide to this use-case, see our [Multi-vendor marketplace guide](/guides/how-to/multi-vendor-marketplaces/).
+
+
+## Specifying channel in the GraphQL API
+
+To specify which channel to use when making an API call, set the `'vendure-token'` header to match the token of the desired Channel.
+
+For example, if we have a UK Channel with the token set to "uk-channel" as shown in this screenshot:
+
+![UK Channel](./channel-token.webp)
+
+Then we can make a GraphQL API call to the UK Channel by setting the `'vendure-token'` header to `'uk-channel'`:
+
+```ts title="GraphQL API call to UK Channel"
+const { loading, error, data } = useQuery(GET_PRODUCT_LIST, {
+    context: {
+        // highlight-start
+        headers: {
+            'vendure-token': 'uk-channel',
+        },
+        // highlight-end
+    },
+});
+```
+
+:::note
+This is an example using Apollo Client in React. The same principle applies to any GraphQL client library - set the `'vendure-token'` header to the token of the desired Channel.
+:::
+
+With the above header set, the API call will be made to the UK Channel, and the response will contain only the entities which are assigned to that Channel.

BIN
docs/docs/guides/core-concepts/channels/variant-prices.webp


BIN
docs/docs/guides/core-concepts/orders/custom-order-ui.webp


+ 260 - 0
docs/docs/guides/core-concepts/orders/index.md

@@ -0,0 +1,260 @@
+---
+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.
+
+The `Order` contains all the information about the order, such as the items (represented by the `OrderLine` entity), shipping address, payment method, shipping method, and so on.
+
+![Order entity](../../developer-guide/data-model/order.webp)
+
+## 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:
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Shop API"
+query ActiveOrder {
+    activeOrder {
+        id
+        // highlight-next-line
+        state
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "activeOrder": {
+      "id": "4",
+      // highlight-next-line
+      "state": "AddingItems"
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+The next possible states can be queried via the [`nextOrderStates`](/reference/graphql-api/shop/queries/#nextorderstates) query:
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Shop API"
+query NextStates {
+  nextOrderStates
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "nextOrderStates": [
+      "ArrangingPayment",
+      "Cancelled"
+    ]
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+
+
+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:
+
+![Default order process](./order-process.webp)
+
+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,
+});
+
+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).
+:::
+
+## 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:
+
+![./custom-order-ui.webp](./custom-order-ui.webp)

BIN
docs/docs/guides/core-concepts/orders/order-process.webp


+ 411 - 0
docs/docs/guides/core-concepts/payment/index.md

@@ -0,0 +1,411 @@
+---
+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 { CancelPaymentErrorResult } from '@vendure/core/src/index';
+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<CreatePaymentResult> => {
+        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<SettlePaymentResult | SettlePaymentErrorResult> => {
+        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<CancelPaymentResult | CancelPaymentErrorResult> => {
+        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 method](./payment-method.webp)
+
+## 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).
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Shop API"
+query GetEligiblePaymentMethods {
+    eligiblePaymentMethods {
+        code
+        name
+        isEligible
+        eligibilityMessage
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "eligiblePaymentMethods": [
+      {
+        "code": "my-payment-method",
+        "name": "My Payment Method",
+        "isEligible": true,
+        "eligibilityMessage": null
+      }
+    ]
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+
+### 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"`.
+ 
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Shop API"
+mutation {
+    addPaymentToOrder(
+        input: {
+            method: "my-payment-method"
+            metadata: { token: "<some token from the payment provider>" }
+        }
+    ) {
+        ... on Order {
+            id
+            code
+            state
+            # ... etc
+        }
+        ... on ErrorResult {
+            errorCode
+            message
+        }
+        ...on PaymentFailedError {
+            paymentErrorMessage
+        }
+        ...on PaymentDeclinedError {
+            paymentErrorMessage
+        }
+        ...on IneligiblePaymentMethodError {
+            eligibilityCheckerMessage
+        }
+    }
+}
+
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "addPaymentToOrder": {
+      "id": "12345",
+      "code": "J9AC5PY13BQGRKTF",
+      "state": "PaymentAuthorized"
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+:::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:
+
+![./payment_sequence_one_step.png](./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.
+
+![./payment_sequence_two_step.png](./payment_sequence_two_step.png)
+
+## 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<boolean> {
+        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.

BIN
docs/docs/guides/core-concepts/payment/payment-method.webp


BIN
docs/docs/guides/core-concepts/payment/payment_sequence_one_step.png


BIN
docs/docs/guides/core-concepts/payment/payment_sequence_two_step.png


+ 315 - 0
docs/docs/guides/core-concepts/promotions/index.md

@@ -0,0 +1,315 @@
+---
+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, orderItem, 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.freeGiftDescription == 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.promotionOptions.promotionActions.push(freeGiftAction);
+        config.customFields.OrderItem.push(
+            {
+                name: 'freeGiftPromotionId',
+                type: 'string',
+                public: true,
+                readonly: true,
+                nullable: true,
+            })
+    }
+})
+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 OrderItem (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.
+
+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, orderItem, orderLine, args, state) {
+        // highlight-next-line
+        const freeItemIds = state.buy_x_get_y_free.freeItemIds;
+        if (idsContainsItem(freeItemIds, orderItem)) {
+            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.

+ 225 - 0
docs/docs/guides/core-concepts/shipping/index.md

@@ -0,0 +1,225 @@
+---
+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.
+
+![Shipping method](./shipping-method.webp)
+
+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.
+
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Shop API"
+query GetEligibleShippingMethods {
+    eligibleShippingMethods {
+        id
+        name
+        price
+        priceWithTax
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```json
+{
+  "data": {
+    "eligibleShippingMethods": [
+      {
+        "id": "1",
+        "name": "Standard Shipping",
+        "price": 500,
+        "priceWithTax": 500
+      },
+      {
+        "id": "2",
+        "name": "Express Shipping",
+        "price": 1000,
+        "priceWithTax": 1000
+      }
+    ]
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+
+
+## 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).
+:::

BIN
docs/docs/guides/core-concepts/shipping/shipping-method.webp


BIN
docs/docs/guides/core-concepts/stock-control/global-stock-control.webp


+ 233 - 0
docs/docs/guides/core-concepts/stock-control/index.md

@@ -0,0 +1,233 @@
+---
+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:
+
+![./global-stock-control.webp](./global-stock-control.webp)
+
+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.
+
+![Stock levels](./stock-levels.webp)
+
+### 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).
+
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```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
+            }
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```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
+      }
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+### 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. Removes stock from `stockOnHand` and adds to `allocated`.
+- [`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` and adds to `stockOnHand`.
+- [`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:
+
+
+<Tabs>
+<TabItem value="Request" label="Request" default>
+
+```graphql title="Admin API"
+query GetStockMovements {
+    productVariant(id: 1) {
+        id
+        name
+        stockLevels {
+            stockLocation {
+                name
+            }
+            stockOnHand
+            stockAllocated
+        }
+        stockMovements {
+            items {
+                ...on StockMovement {
+                    createdAt
+                    type
+                    quantity
+                }
+            }
+        }
+    }
+}
+```
+
+</TabItem>
+<TabItem value="Response" label="Response">
+
+```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
+          }
+        ]
+      }
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>

BIN
docs/docs/guides/core-concepts/stock-control/stock-levels.webp


+ 18 - 12
docs/docs/guides/developer-guide-old/taxes.md → docs/docs/guides/core-concepts/taxes/index.mdx

@@ -2,12 +2,11 @@
 title: "Taxes"
 showtoc: true
 ---
-# Taxes
 
-Most e-commerce applications need to correctly handle taxes such as sales tax or value added tax (VAT). In Vendure, tax handling consists of:
+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]({{< relref "zone" >}}). E.g., the tax rate for "standard" goods in the UK Zone is 20%.
+* **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.
@@ -37,25 +36,32 @@ query {
 
 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 taxes on OrderItems
+## Calculating tax on order lines
 
 When a customer adds an item to the Order, the following logic takes place:
 
-1. The price of the OrderItem, and whether or not that price is inclusive of tax, is determined according to the configured [OrderItemPriceCalculationStrategy]({{< relref "order-item-price-calculation-strategy" >}}).
-2. The active tax Zone is determined based on the configured [TaxZoneStrategy]({{< relref "tax-zone-strategy" >}}).
+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/).
 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]({{< relref "tax-line-calculation-strategy" >}}) is called, which will return one or more [TaxLines]({{< relref "/reference/graphql-api/shop/object-types" >}}#taxline).
-5. The final `priceWithTax` of the OrderItem is calculated based on all the above.
+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 taxes on shipping
+## Calculating tax on shipping
 
-The taxes on shipping is calculated by the [ShippingCalculator]({{< relref "shipping-calculator" >}}) of the Order's selected [ShippingMethod]({{< relref "shipping-method" >}}).
+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 configuration properties related to taxes:
+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';
 
-```ts
 export const config: VendureConfig = {
   taxOptions: {
     taxZoneStrategy: new DefaultTaxZoneStrategy(),

+ 0 - 314
docs/docs/guides/developer-guide-old/promotions.md

@@ -1,314 +0,0 @@
----
-title: 'Promotions'
-showtoc: true
----
-
-# 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]({{< relref "/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 of them are _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]({{< relref "promotion-condition" >}}).
-Here is an annotated example of one of the built-in PromotionsConditions:
-
-```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 PromotionConditions are then passed into the VendureConfig [PromotionOptions]({{< relref "promotion-options" >}}) to make them available when setting up Promotions:
-
-```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`]({{< relref "promotion-action" >}}#promotionitemaction) applies a discount on the OrderItem level, i.e. it would be used for a promotion like "50% off USB cables".
--   [`PromotionOrderAction`]({{< relref "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`]({{< relref "promotion-action" >}}#promotionshippingaction) applies a discount on the shipping, i.e. it would be used for a promotion like "free shipping".
-
-Their implementations are similar, with the difference being the arguments passed to the `execute()` function of each.
-
-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) {
-      return -order.subTotal * (args.discount / 100);
-  },
-});
-
-```
-
-Custom PromotionActions are then passed into the VendureConfig [PromotionOptions]({{< relref "promotion-options" >}}) to make them available when setting up Promotions:
-
-```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 introduces 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
-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, orderItem, 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.freeGiftDescription == 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.promotionOptions.promotionActions.push(freeGiftAction);
-    config.customFields.OrderItem.push(
-    {
-      name: 'freeGiftPromotionId',
-      type: 'string',
-      public: true,
-      readonly: true,
-      nullable: true,
-    })
-  }
-})
-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 OrderItem (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.
-
-Instead, we can say that the PromotionAction _depends_ on the PromotionCondition:
-
-```ts {hl_lines=[8,10]}
-export const buy1Get1FreeAction = new PromotionItemAction({
-  code: 'buy_1_get_1_free',
-  description: [{
-    languageCode: LanguageCode.en,
-    value: 'Buy 1, get 1 free',
-  }],
-  args: {},
-  conditions: [buyXGetYFreeCondition],
-  execute(ctx, orderItem, orderLine, args, state) {
-      const freeItemIds = state.buy_x_get_y_free.freeItemIds;
-      if (idsContainsItem(freeItemIds, orderItem)) {
-          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 {hl_lines=[15]}
-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;
-    }  
-    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 [ConfigurableOperationDef Dependency Injection guide]({{< relref "configurable-operation-def" >}}#dependency-injection) for details.

+ 0 - 165
docs/docs/guides/developer-guide-old/shipping.md

@@ -1,165 +0,0 @@
----
-title: "Shipping & Fulfillment"
-showtoc: true
----
-# Shipping & Fulfillment
-
-Shipping in Vendure is handled by [ShippingMethods]({{< relref "shipping-method" >}}). Multiple ShippingMethods can be set up and then your storefront can query [`eligibleShippingMethods`]({{< relref "/reference/graphql-api/shop/queries" >}}#eligibleshippingmethods) to find out which ones can be applied to the active order.
-
-A ShippingMethod is composed of a **checker** and a **calculator**. 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 the shipping cost.
-
-## Creating a custom checker
-
-Custom checkers can be created by defining a [`ShippingEligibilityChecker` object]({{< relref "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
-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 as any).weight * l.quantity)
-      .reduce((total, lineWeight) => total + lineWeight, 0);
-    
-    return totalWeight <= args.maxWeight;
-  },
-});
-```
-Custom checkers are then passed into the VendureConfig [ShippingOptions]({{< relref "shipping-options" >}}) to make them available when setting up new ShippingMethods:
-
-```ts
-import { defaultShippingEligibilityChecker, VendureConfig } from '@vendure/core';
-import { maxWeightChecker } from './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]({{< relref "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
-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]({{< relref "shipping-options" >}}) 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,
-    ],
-  }
-}
-```
-
-{{% alert %}}
-**Dependency Injection**
-
-If your ShippingEligibilityChecker or ShippingCalculator needs access to the database or other providers, see the [ConfigurableOperationDef Dependency Injection guide]({{< relref "configurable-operation-def" >}}#dependency-injection).
-{{< /alert >}}
-
-## 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.
-
-### 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]({{< relref "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]({{< relref "fsm" >}}) and by default, a Fulfillment can be in one of the [following states]({{< relref "fulfillment-state" >}}):
-
-* `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]({{< relref "fulfillment-process" >}}) and passing it to your VendureConfig:
-
-```ts
-export const config: VendureConfig = {
-  // ...
-  shippingOptions: {
-    customFulfillmentProcess: [myCustomFulfillmentProcess],
-  },
-};
-```
-
-For a more detailed look at how custom processes are used, see the [Customizing The Order Process guide]({{< relref "customizing-the-order-process" >}}).

+ 0 - 94
docs/docs/guides/developer-guide-old/vendure-worker.md

@@ -1,94 +0,0 @@
----
-title: "Vendure Worker"
-showtoc: true
----
-
-# Vendure Worker
- 
-The Vendure Worker is a process responsible for running computationally intensive or otherwise long-running tasks in the background. For example, updating a search index or sending emails. Running such tasks in the background allows the server to stay responsive, since a response can be returned immediately without waiting for the slower tasks to complete. 
-
-Put another way, the Worker executes jobs registered with the [JobQueueService]({{< relref "job-queue-service" >}}).
-
-The worker is started by calling the [`bootstrapWorker()`]({{< relref "bootstrap-worker" >}}) function with the same configuration as is passed to the main server `bootstrap()`:
-
-```ts
-import { bootstrapWorker } from '@vendure/core';
-import { config } from './vendure-config';
-
-bootstrapWorker(config)
-  // We must explicitly start the job queue in order for this
-  // worker instance to start listening for and processing jobs.
-  .then(worker => worker.startJobQueue())
-  .catch(err => {
-    console.log(err);
-    process.exit(1);
-  });
-```
-
-## Underlying architecture
-
-The Worker is a [NestJS standalone application](https://docs.nestjs.com/standalone-applications). This means it is almost identical to the main server app, but does not have any network layer listening for requests. The server communicates with the worker via a "job queue" architecture. The exact implementation of the job queue is dependent on the configured `JobQueueStrategy` - see the [Job Queue guide]({{< relref "job-queue" >}}) for more details.
-
-## Multiple workers
-
-It is possible to run multiple workers in parallel to better handle heavy loads. Using the [JobQueueOptions.activeQueues]({{< relref "job-queue-options" >}}#activequeues) configuration, it is even possible to have particular workers dedicated to one or more specific types of jobs.
-For example, if your application does video transcoding, you might want to set up a dedicated worker just for that task:
-
-```ts
-import { bootstrapWorker, mergeConfig } from '@vendure/core';
-import { config } from './vendure-config';
-
-const videoWorkerConfig = mergeConfig(config, {
-  jobQueueOptions: {
-    activeQueues: ['transcode-video'],
-  }
-})
-
-bootstrapWorker(videoWorkerConfig)
-  .then(worker => worker.startJobQueue())
-  .catch(err => {
-    console.log(err);
-    process.exit(1);
-  });
-```
-
-## Running jobs on the main process
-
-It is possible to run jobs from the Job Queue on the main server. This is mainly used for testing and automated tasks, and is not advised for production use, since it negates the benefits of running long tasks off of the main process. To do so, you need to manually start the JobQueueService:
-
-```ts
-import { bootstrap, JobQueueService } from '@vendure/core';
-import { config } from './vendure-config';
-
-bootstrap(config)
-  .then(app => app.get(JobQueueService).start())
-  .catch(err => {
-    console.log(err);
-    process.exit(1);
-  });
-```
-
-## Running custom code on the worker
-
-If you are authoring a [Vendure plugin]({{< relref "/guides/plugins" >}}) to implement custom functionality, you can also make use of the worker process in order to handle long-running or computationally-demanding tasks. See the [Plugin Examples]({{< relref "plugin-examples" >}}#running-processes-on-the-worker) page for an example of this.
-
-## ProcessContext
-
-Sometimes your code may need to be aware of whether it is being run as part of a server or worker process. In this case you can inject the [ProcessContext]({{< relref "process-context" >}}) provider and query it like this:
-
-```ts
-import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
-import { ProcessContext } from '@vendure/core';
-
-@Injectable()
-export class MyService implements OnApplicationBootstrap {
-  constructor(private processContext: ProcessContext) {}
-
-  onApplicationBootstrap() {
-    if (this.processContext.isServer) {
-      // code which will only execute when running in
-      // the server process
-    }
-  }
-}
-```

+ 4 - 19
docs/docs/guides/developer-guide/data-model/index.mdx

@@ -160,28 +160,13 @@ A [`Channel`](/reference/typescript-api/entities/channel/) represents a distinct
 - **Distinct sales channels**: Each channel can represent a sales channel of a single business, with one channel for the online
   store, one for selling via Amazon, one for selling via Facebook etc.
 
+:::info
+Channels are covered in much more detail in the dedicated [Channels guide](/guides/core-concepts/channels/).
+:::
+
 There is _always_ the **default channel**, which cannot be deleted. This channel is the parent of all other channels, and also
 contains all entities in the entire application, no matter which channel they are associated with. This means that channel-aware
 entities are always associated with the default channel and _may_ additionally be associated with other channels.
 
 ![Channels](./channels.webp)
 
-Many entities are channel-aware, meaning that they can be associated with a multiple channels. The following entities are channel-aware:
-
-- [`Asset`](/reference/typescript-api/entities/asset/)
-- [`Collection`](/reference/typescript-api/entities/collection/)
-- [`Customer`](/reference/typescript-api/entities/customer-group/)
-- [`Facet`](/reference/typescript-api/entities/facet/)
-- [`FacetValue`](/reference/typescript-api/entities/facet-value/)
-- [`Order`](/reference/typescript-api/entities/order/)
-- [`PaymentMethod`](/reference/typescript-api/entities/payment-method/)
-- [`Product`](/reference/typescript-api/entities/product/)
-- [`ProductVariant`](/reference/typescript-api/entities/product-variant/)
-- [`Promotion`](/reference/typescript-api/entities/promotion/)
-- [`Role`](/reference/typescript-api/entities/role/)
-- [`ShippingMethod`](/reference/typescript-api/entities/shipping-method/)
-- [`StockLocation`](/reference/typescript-api/entities/stock-location/)
-
-Each channel is also assigned a single [`Seller`](/reference/typescript-api/entities/seller/). This entity is used to represent
-the vendor or seller of the products in the channel. This is useful for implementing a marketplace, where each channel represents
-a distinct vendor.

+ 0 - 0
docs/docs/guides/developer-guide-old/uploading-files.md → docs/docs/guides/how-to/uploading-files.md