Bladeren bron

feat(core): Add OrderInterceptor API (#3233)

Closes #2123
Michael Bromley 1 jaar geleden
bovenliggende
commit
7706e35bbd
100 gewijzigde bestanden met toevoegingen van 4566 en 2810 verwijderingen
  1. 12 0
      docs/docs/guides/core-concepts/orders/index.md
  2. 41 41
      docs/docs/reference/admin-ui-api/bulk-actions/bulk-action.md
  3. 9 9
      docs/docs/reference/admin-ui-api/list-detail-views/detail-component-with-resolver.md
  4. 7 7
      docs/docs/reference/admin-ui-api/list-detail-views/typed-base-list-component.md
  5. 7 7
      docs/docs/reference/admin-ui-api/react-components/form-field.md
  6. 12 12
      docs/docs/reference/core-plugins/asset-server-plugin/asset-server-options.md
  7. 2 1
      docs/docs/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy.md
  8. 114 114
      docs/docs/reference/core-plugins/elasticsearch-plugin/elasticsearch-options.md
  9. 1 1
      docs/docs/reference/core-plugins/elasticsearch-plugin/index.md
  10. 23 23
      docs/docs/reference/core-plugins/harden-plugin/harden-plugin-options.md
  11. 116 116
      docs/docs/reference/core-plugins/payments-plugin/mollie-plugin.md
  12. 199 166
      docs/docs/reference/core-plugins/payments-plugin/stripe-plugin.md
  13. 9 9
      docs/docs/reference/core-plugins/stellate-plugin/purge-rule.md
  14. 1 0
      docs/docs/reference/graphql-api/admin/enums.md
  15. 5 9
      docs/docs/reference/graphql-api/admin/input-types.md
  16. 24 0
      docs/docs/reference/graphql-api/admin/mutations.md
  17. 19 14
      docs/docs/reference/graphql-api/admin/object-types.md
  18. 1 0
      docs/docs/reference/graphql-api/shop/enums.md
  19. 19 14
      docs/docs/reference/graphql-api/shop/object-types.md
  20. 1 1
      docs/docs/reference/typescript-api/assets/asset-options.md
  21. 1 1
      docs/docs/reference/typescript-api/auth/auth-options.md
  22. 1 1
      docs/docs/reference/typescript-api/auth/cookie-options.md
  23. 1 1
      docs/docs/reference/typescript-api/auth/superadmin-credentials.md
  24. 1 1
      docs/docs/reference/typescript-api/common/currency-code.md
  25. 1 1
      docs/docs/reference/typescript-api/common/entity-relation-paths.md
  26. 1 1
      docs/docs/reference/typescript-api/common/job-state.md
  27. 1 1
      docs/docs/reference/typescript-api/common/language-code.md
  28. 1 1
      docs/docs/reference/typescript-api/common/permission.md
  29. 9 9
      docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md
  30. 1 1
      docs/docs/reference/typescript-api/configuration/api-options.md
  31. 13 13
      docs/docs/reference/typescript-api/configuration/entity-duplicator.md
  32. 1 1
      docs/docs/reference/typescript-api/configuration/entity-options.md
  33. 1 1
      docs/docs/reference/typescript-api/configuration/runtime-vendure-config.md
  34. 1 1
      docs/docs/reference/typescript-api/configuration/system-options.md
  35. 1 1
      docs/docs/reference/typescript-api/configuration/vendure-config.md
  36. 2 1
      docs/docs/reference/typescript-api/custom-fields/index.md
  37. 2 1
      docs/docs/reference/typescript-api/data-access/calculated.md
  38. 48 48
      docs/docs/reference/typescript-api/data-access/transactional-connection.md
  39. 1 1
      docs/docs/reference/typescript-api/default-search-plugin/index.md
  40. 30 30
      docs/docs/reference/typescript-api/entities/promotion.md
  41. 44 44
      docs/docs/reference/typescript-api/events/event-types.md
  42. 14 14
      docs/docs/reference/typescript-api/fulfillment/fulfillment-process.md
  43. 5 5
      docs/docs/reference/typescript-api/import-export/default-asset-import-strategy.md
  44. 1 1
      docs/docs/reference/typescript-api/import-export/import-export-options.md
  45. 33 33
      docs/docs/reference/typescript-api/import-export/import-parser.md
  46. 1 1
      docs/docs/reference/typescript-api/job-queue/job-queue-options.md
  47. 2 1
      docs/docs/reference/typescript-api/job-queue/types.md
  48. 25 25
      docs/docs/reference/typescript-api/orders/guest-checkout-strategy.md
  49. 254 0
      docs/docs/reference/typescript-api/orders/order-interceptor.md
  50. 2 1
      docs/docs/reference/typescript-api/orders/order-item-price-calculation-strategy.md
  51. 8 2
      docs/docs/reference/typescript-api/orders/order-options.md
  52. 1 1
      docs/docs/reference/typescript-api/payment/payment-options.md
  53. 1 1
      docs/docs/reference/typescript-api/products-stock/catalog-options.md
  54. 1 1
      docs/docs/reference/typescript-api/promotions/promotion-options.md
  55. 13 13
      docs/docs/reference/typescript-api/request/request-context-service.md
  56. 11 11
      docs/docs/reference/typescript-api/service-helpers/slug-validator.md
  57. 25 25
      docs/docs/reference/typescript-api/services/customer-service.md
  58. 11 11
      docs/docs/reference/typescript-api/services/fulfillment-service.md
  59. 1 1
      docs/docs/reference/typescript-api/services/order-service.md
  60. 5 5
      docs/docs/reference/typescript-api/services/payment-method-service.md
  61. 21 21
      docs/docs/reference/typescript-api/services/user-service.md
  62. 1 1
      docs/docs/reference/typescript-api/shipping/shipping-options.md
  63. 1 1
      docs/docs/reference/typescript-api/tax/tax-options.md
  64. 35 10
      docs/docs/reference/typescript-api/testing/simple-graph-qlclient.md
  65. 28 25
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  66. 349 260
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  67. 435 457
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  68. 653 675
      packages/common/src/generated-shop-types.ts
  69. 20 17
      packages/common/src/generated-types.ts
  70. 125 2
      packages/core/e2e/custom-field-struct.e2e-spec.ts
  71. 435 457
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  72. 62 8
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  73. 9 0
      packages/core/e2e/graphql/shop-definitions.ts
  74. 290 0
      packages/core/e2e/order-interceptor.e2e-spec.ts
  75. 1 1
      packages/core/src/api/common/validate-custom-field-value.ts
  76. 30 3
      packages/core/src/api/config/graphql-custom-fields.ts
  77. 9 0
      packages/core/src/api/schema/common/common-error-results.graphql
  78. 2 1
      packages/core/src/api/schema/common/common-types.graphql
  79. 14 1
      packages/core/src/common/error/generated-graphql-admin-errors.ts
  80. 14 1
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  81. 2 0
      packages/core/src/config/config.module.ts
  82. 1 0
      packages/core/src/config/default-config.ts
  83. 1 0
      packages/core/src/config/index.ts
  84. 250 0
      packages/core/src/config/order/order-interceptor.ts
  85. 11 1
      packages/core/src/config/vendure-config.ts
  86. 1 0
      packages/core/src/i18n/messages/en.json
  87. 12 6
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  88. 40 2
      packages/core/src/service/services/order.service.ts
  89. 5 4
      packages/core/src/service/services/payment.service.ts
  90. 111 0
      packages/dev-server/example-plugins/minimum-order-quantity/order-quantity-limits.plugin.ts
  91. 13 0
      packages/dev-server/example-plugins/product-bundles/README.md
  92. 73 0
      packages/dev-server/example-plugins/product-bundles/api/api-extensions.ts
  93. 108 0
      packages/dev-server/example-plugins/product-bundles/api/product-bundle-admin.resolver.ts
  94. 32 0
      packages/dev-server/example-plugins/product-bundles/api/product-bundle-shop.resolver.ts
  95. 17 0
      packages/dev-server/example-plugins/product-bundles/config/bundle-order-interceptor.ts
  96. 4 0
      packages/dev-server/example-plugins/product-bundles/constants.ts
  97. 29 0
      packages/dev-server/example-plugins/product-bundles/entities/product-bundle-item.entity.ts
  98. 20 0
      packages/dev-server/example-plugins/product-bundles/entities/product-bundle.entity.ts
  99. 48 0
      packages/dev-server/example-plugins/product-bundles/product-bundles.plugin.ts
  100. 56 0
      packages/dev-server/example-plugins/product-bundles/services/product-bundle-item.service.ts

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

@@ -352,3 +352,15 @@ If you have defined custom order states, the Admin UI will allow you to manually
 order from one state to another:
 
 ![./custom-order-ui.webp](./custom-order-ui.webp)
+
+## Order Interceptors
+
+Vendure v3.1 introduces the concept of [Order Interceptors](/reference/typescript-api/orders/order-interceptor/). 
+These are a way to intercept operations that add, modify or remove order lines. Examples use-cases include:
+
+* Preventing certain products from being added to the order based on some criteria, e.g. if the  product is already in another active order.
+* Enforcing a minimum or maximum quantity of a given product in the order
+* Using a CAPTCHA to prevent automated order creation
+
+Check the [Order Interceptor](/reference/typescript-api/orders/order-interceptor/) docs for more information as well as a complete
+example of how to implement an interceptor.

+ 41 - 41
docs/docs/reference/admin-ui-api/bulk-actions/bulk-action.md

@@ -13,16 +13,16 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/bulk-action-registry/bulk-action-types.ts" sourceLine="99" packageName="@vendure/admin-ui" since="1.8.0" />
 
-Configures a bulk action which can be performed on all selected items in a list view.
-
+Configures a bulk action which can be performed on all selected items in a list view.
+
 For a full example, see the <a href='/reference/admin-ui-api/bulk-actions/register-bulk-action#registerbulkaction'>registerBulkAction</a> docs.
 
 ```ts title="Signature"
 interface BulkAction<ItemType = any, ComponentType = any> {
     location: BulkActionLocationId;
     label: string;
-    getTranslationVars?: (
-        context: BulkActionFunctionContext<ItemType, ComponentType>,
+    getTranslationVars?: (
+        context: BulkActionFunctionContext<ItemType, ComponentType>,
     ) => Record<string, string | number> | Promise<Record<string, string | number>>;
     icon?: string;
     iconClass?: string;
@@ -46,26 +46,26 @@ interface BulkAction<ItemType = any, ComponentType = any> {
 
 ### getTranslationVars
 
-<MemberInfo kind="property" type={`(         context: <a href='/reference/admin-ui-api/bulk-actions/bulk-action#bulkactionfunctioncontext'>BulkActionFunctionContext</a>&#60;ItemType, ComponentType&#62;,     ) =&#62; Record&#60;string, string | number&#62; | Promise&#60;Record&#60;string, string | number&#62;&#62;`}   />
+<MemberInfo kind="property" type={`(
         context: <a href='/reference/admin-ui-api/bulk-actions/bulk-action#bulkactionfunctioncontext'>BulkActionFunctionContext</a>&#60;ItemType, ComponentType&#62;,
     ) =&#62; Record&#60;string, string | number&#62; | Promise&#60;Record&#60;string, string | number&#62;&#62;`}   />
 
-An optional function that should resolve to a map of translation variables which can be
+An optional function that should resolve to a map of translation variables which can be
 used when translating the `label` string.
 ### icon
 
 <MemberInfo kind="property" type={`string`}   />
 
-A valid [Clarity Icons](https://core.clarity.design/foundation/icons/shapes/) icon shape, e.g.
+A valid [Clarity Icons](https://core.clarity.design/foundation/icons/shapes/) icon shape, e.g.
 "cog", "user", "info-standard".
 ### iconClass
 
 <MemberInfo kind="property" type={`string`}   />
 
-A class to be added to the icon element. Examples:
-
-- is-success
-- is-danger
-- is-warning
-- is-info
+A class to be added to the icon element. Examples:
+
+- is-success
+- is-danger
+- is-warning
+- is-info
 - is-highlight
 ### onClick
 
@@ -76,10 +76,10 @@ Defines the logic that executes when the bulk action button is clicked.
 
 <MemberInfo kind="property" type={`(context: <a href='/reference/admin-ui-api/bulk-actions/bulk-action#bulkactionfunctioncontext'>BulkActionFunctionContext</a>&#60;ItemType, ComponentType&#62;) =&#62; boolean | Promise&#60;boolean&#62;`}   />
 
-A function that determines whether this bulk action item should be displayed in the menu.
-If not defined, the item will always be displayed.
-
-This function will be invoked each time the selection is changed, so try to avoid expensive code
+A function that determines whether this bulk action item should be displayed in the menu.
+If not defined, the item will always be displayed.
+
+This function will be invoked each time the selection is changed, so try to avoid expensive code
 running here.
 
 *Example*
@@ -130,25 +130,25 @@ registerBulkAction({
 A valid location of a list view that supports the bulk actions API.
 
 ```ts title="Signature"
-type BulkActionLocationId = | 'product-list'
-    | 'facet-list'
-    | 'collection-list'
-    | 'customer-list'
-    | 'customer-group-list'
-    | 'customer-group-members-list'
-    | 'customer-group-members-picker-list'
-    | 'promotion-list'
-    | 'seller-list'
-    | 'channel-list'
-    | 'administrator-list'
-    | 'role-list'
-    | 'shipping-method-list'
-    | 'stock-location-list'
-    | 'payment-method-list'
-    | 'tax-category-list'
-    | 'tax-rate-list'
-    | 'zone-list'
-    | 'zone-members-list'
+type BulkActionLocationId = | 'product-list'
+    | 'facet-list'
+    | 'collection-list'
+    | 'customer-list'
+    | 'customer-group-list'
+    | 'customer-group-members-list'
+    | 'customer-group-members-picker-list'
+    | 'promotion-list'
+    | 'seller-list'
+    | 'channel-list'
+    | 'administrator-list'
+    | 'role-list'
+    | 'shipping-method-list'
+    | 'stock-location-list'
+    | 'payment-method-list'
+    | 'tax-category-list'
+    | 'tax-rate-list'
+    | 'zone-list'
+    | 'zone-members-list'
     | string
 ```
 
@@ -157,7 +157,7 @@ type BulkActionLocationId = | 'product-list'
 
 <GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/providers/bulk-action-registry/bulk-action-types.ts" sourceLine="43" packageName="@vendure/admin-ui" since="1.8.0" />
 
-This is the argument which gets passed to the `getTranslationVars` and `isVisible` functions
+This is the argument which gets passed to the `getTranslationVars` and `isVisible` functions
 of the BulkAction definition.
 
 ```ts title="Signature"
@@ -180,15 +180,15 @@ An array of the selected items from the list.
 
 <MemberInfo kind="property" type={`ComponentType`}   />
 
-The component instance that is hosting the list view. For instance,
-`ProductListComponent`. This can be used to call methods on the instance,
-e.g. calling `hostComponent.refresh()` to force a list refresh after
+The component instance that is hosting the list view. For instance,
+`ProductListComponent`. This can be used to call methods on the instance,
+e.g. calling `hostComponent.refresh()` to force a list refresh after
 deleting the selected items.
 ### injector
 
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/common/injector#injector'>Injector</a>`}   />
 
-The Angular [Injector](https://angular.io/api/core/Injector) which can be used
+The Angular [Injector](https://angular.io/api/core/Injector) which can be used
 to get service instances which might be needed in the click handler.
 ### route
 

+ 9 - 9
docs/docs/reference/admin-ui-api/list-detail-views/detail-component-with-resolver.md

@@ -13,8 +13,8 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/common/base-detail.component.ts" sourceLine="256" packageName="@vendure/admin-ui" />
 
-A helper function for creating tabs that point to a <a href='/reference/admin-ui-api/list-detail-views/typed-base-detail-component#typedbasedetailcomponent'>TypedBaseDetailComponent</a>. This takes
-care of the route resolver parts so that the detail component automatically has access to the
+A helper function for creating tabs that point to a <a href='/reference/admin-ui-api/list-detail-views/typed-base-detail-component#typedbasedetailcomponent'>TypedBaseDetailComponent</a>. This takes
+care of the route resolver parts so that the detail component automatically has access to the
 correct resolved detail data.
 
 *Example*
@@ -40,17 +40,17 @@ export class ProductSpecsUiExtensionModule {}
 ```
 
 ```ts title="Signature"
-function detailComponentWithResolver<T extends TypedDocumentNode<any, { id: string }>, Field extends keyof ResultOf<T>, R extends Field>(config: {
-    component: Type<TypedBaseDetailComponent<T, Field>>;
-    query: T;
-    entityKey: R;
-    getBreadcrumbs?: (entity: ResultOf<T>[R]) => BreadcrumbValue;
-    variables?: T extends TypedDocumentNode<any, infer V> ? Omit<V, 'id'> : never;
+function detailComponentWithResolver<T extends TypedDocumentNode<any, { id: string }>, Field extends keyof ResultOf<T>, R extends Field>(config: {
+    component: Type<TypedBaseDetailComponent<T, Field>>;
+    query: T;
+    entityKey: R;
+    getBreadcrumbs?: (entity: ResultOf<T>[R]) => BreadcrumbValue;
+    variables?: T extends TypedDocumentNode<any, infer V> ? Omit<V, 'id'> : never;
 }): void
 ```
 Parameters
 
 ### config
 
-<MemberInfo kind="parameter" type={`{     component: Type&#60;<a href='/reference/admin-ui-api/list-detail-views/typed-base-detail-component#typedbasedetailcomponent'>TypedBaseDetailComponent</a>&#60;T, Field&#62;&#62;;     query: T;     entityKey: R;     getBreadcrumbs?: (entity: ResultOf&#60;T&#62;[R]) =&#62; BreadcrumbValue;     variables?: T extends TypedDocumentNode&#60;any, infer V&#62; ? Omit&#60;V, 'id'&#62; : never; }`} />
+<MemberInfo kind="parameter" type={`{
     component: Type&#60;<a href='/reference/admin-ui-api/list-detail-views/typed-base-detail-component#typedbasedetailcomponent'>TypedBaseDetailComponent</a>&#60;T, Field&#62;&#62;;
     query: T;
     entityKey: R;
     getBreadcrumbs?: (entity: ResultOf&#60;T&#62;[R]) =&#62; BreadcrumbValue;
     variables?: T extends TypedDocumentNode&#60;any, infer V&#62; ? Omit&#60;V, 'id'&#62; : never;
 }`} />
 

+ 7 - 7
docs/docs/reference/admin-ui-api/list-detail-views/typed-base-list-component.md

@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/admin-ui/src/lib/core/src/common/base-list.component.ts" sourceLine="217" packageName="@vendure/admin-ui" />
 
-A version of the <a href='/reference/admin-ui-api/list-detail-views/base-list-component#baselistcomponent'>BaseListComponent</a> which is designed to be used with a
+A version of the <a href='/reference/admin-ui-api/list-detail-views/base-list-component#baselistcomponent'>BaseListComponent</a> which is designed to be used with a
 [TypedDocumentNode](https://the-guild.dev/graphql/codegen/plugins/typescript/typed-document-node).
 
 ```ts title="Signature"
@@ -27,11 +27,11 @@ class TypedBaseListComponent<T extends TypedDocumentNode<any, Vars>, Field exten
     protected dataTableConfigService = inject(DataTableConfigService);
     protected dataTableListId: string | undefined;
     constructor()
-    configure(config: {
-        document: T;
-        getItems: (data: ResultOf<T>) => { items: Array<ItemOf<ResultOf<T>, Field>>; totalItems: number };
-        setVariables?: (skip: number, take: number) => VariablesOf<T>;
-        refreshListOnChanges?: Array<Observable<any>>;
+    configure(config: {
+        document: T;
+        getItems: (data: ResultOf<T>) => { items: Array<ItemOf<ResultOf<T>, Field>>; totalItems: number };
+        setVariables?: (skip: number, take: number) => VariablesOf<T>;
+        refreshListOnChanges?: Array<Observable<any>>;
     }) => ;
     ngOnInit() => ;
     createFilterCollection() => DataTableFilterCollection<NonNullable<NonNullable<Vars['options']>['filter']>>;
@@ -96,7 +96,7 @@ class TypedBaseListComponent<T extends TypedDocumentNode<any, Vars>, Field exten
 
 ### configure
 
-<MemberInfo kind="method" type={`(config: {         document: T;         getItems: (data: ResultOf&#60;T&#62;) =&#62; { items: Array&#60;ItemOf&#60;ResultOf&#60;T&#62;, Field&#62;&#62;; totalItems: number };         setVariables?: (skip: number, take: number) =&#62; VariablesOf&#60;T&#62;;         refreshListOnChanges?: Array&#60;Observable&#60;any&#62;&#62;;     }) => `}   />
+<MemberInfo kind="method" type={`(config: {
         document: T;
         getItems: (data: ResultOf&#60;T&#62;) =&#62; { items: Array&#60;ItemOf&#60;ResultOf&#60;T&#62;, Field&#62;&#62;; totalItems: number };
         setVariables?: (skip: number, take: number) =&#62; VariablesOf&#60;T&#62;;
         refreshListOnChanges?: Array&#60;Observable&#60;any&#62;&#62;;
     }) => `}   />
 
 
 ### ngOnInit

+ 7 - 7
docs/docs/reference/admin-ui-api/react-components/form-field.md

@@ -30,17 +30,17 @@ export function MyReactComponent() {
 ```
 
 ```ts title="Signature"
-function FormField(props: PropsWithChildren<{
-        for?: string;
-        label?: string;
-        tooltip?: string;
-        invalid?: boolean;
-        errorMessage?: string;
+function FormField(props: PropsWithChildren<{
+        for?: string;
+        label?: string;
+        tooltip?: string;
+        invalid?: boolean;
+        errorMessage?: string;
     }>): void
 ```
 Parameters
 
 ### props
 
-<MemberInfo kind="parameter" type={`PropsWithChildren&#60;{         for?: string;         label?: string;         tooltip?: string;         invalid?: boolean;         errorMessage?: string;     }&#62;`} />
+<MemberInfo kind="parameter" type={`PropsWithChildren&#60;{
         for?: string;
         label?: string;
         tooltip?: string;
         invalid?: boolean;
         errorMessage?: string;
     }&#62;`} />
 

+ 12 - 12
docs/docs/reference/core-plugins/asset-server-plugin/asset-server-options.md

@@ -25,8 +25,8 @@ interface AssetServerOptions {
     presets?: ImageTransformPreset[];
     namingStrategy?: AssetNamingStrategy;
     previewStrategy?: AssetPreviewStrategy;
-    storageStrategyFactory?: (
-        options: AssetServerOptions,
+    storageStrategyFactory?: (
+        options: AssetServerOptions,
     ) => AssetStorageStrategy | Promise<AssetStorageStrategy>;
     cacheHeader?: CacheConfig | string;
 }
@@ -48,12 +48,12 @@ The local directory to which assets will be uploaded when using the <a href='/re
 
 <MemberInfo kind="property" type={`string | ((ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, identifier: string) =&#62; string)`}   />
 
-The complete URL prefix of the asset files. For example, "https://demo.vendure.io/assets/". A
-function can also be provided to handle more complex cases, such as serving multiple domains
-from a single server. In this case, the function should return a string url prefix.
-
-If not provided, the plugin will attempt to guess based off the incoming
-request and the configured route. However, in all but the simplest cases,
+The complete URL prefix of the asset files. For example, "https://demo.vendure.io/assets/". A
+function can also be provided to handle more complex cases, such as serving multiple domains
+from a single server. In this case, the function should return a string url prefix.
+
+If not provided, the plugin will attempt to guess based off the incoming
+request and the configured route. However, in all but the simplest cases,
 this guess may not yield correct results.
 ### previewMaxWidth
 
@@ -79,19 +79,19 @@ Defines how asset files and preview images are named before being saved.
 
 <MemberInfo kind="property" type={`<a href='/reference/typescript-api/assets/asset-preview-strategy#assetpreviewstrategy'>AssetPreviewStrategy</a>`}  since="1.7.0"  />
 
-Defines how previews are generated for a given Asset binary. By default, this uses
+Defines how previews are generated for a given Asset binary. By default, this uses
 the <a href='/reference/core-plugins/asset-server-plugin/sharp-asset-preview-strategy#sharpassetpreviewstrategy'>SharpAssetPreviewStrategy</a>
 ### storageStrategyFactory
 
-<MemberInfo kind="property" type={`(         options: <a href='/reference/core-plugins/asset-server-plugin/asset-server-options#assetserveroptions'>AssetServerOptions</a>,     ) =&#62; <a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a> | Promise&#60;<a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a>&#62;`} default={`() =&#62; <a href='/reference/core-plugins/asset-server-plugin/local-asset-storage-strategy#localassetstoragestrategy'>LocalAssetStorageStrategy</a>`}   />
+<MemberInfo kind="property" type={`(
         options: <a href='/reference/core-plugins/asset-server-plugin/asset-server-options#assetserveroptions'>AssetServerOptions</a>,
     ) =&#62; <a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a> | Promise&#60;<a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a>&#62;`} default={`() =&#62; <a href='/reference/core-plugins/asset-server-plugin/local-asset-storage-strategy#localassetstoragestrategy'>LocalAssetStorageStrategy</a>`}   />
 
-A function which can be used to configure an <a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a>. This is useful e.g. if you wish to store your assets
+A function which can be used to configure an <a href='/reference/typescript-api/assets/asset-storage-strategy#assetstoragestrategy'>AssetStorageStrategy</a>. This is useful e.g. if you wish to store your assets
 using a cloud storage provider. By default, the <a href='/reference/core-plugins/asset-server-plugin/local-asset-storage-strategy#localassetstoragestrategy'>LocalAssetStorageStrategy</a> is used.
 ### cacheHeader
 
 <MemberInfo kind="property" type={`<a href='/reference/core-plugins/asset-server-plugin/cache-config#cacheconfig'>CacheConfig</a> | string`} default={`'public, max-age=15552000'`}  since="1.9.3"  />
 
-Configures the `Cache-Control` directive for response to control caching in browsers and shared caches (e.g. Proxies, CDNs).
+Configures the `Cache-Control` directive for response to control caching in browsers and shared caches (e.g. Proxies, CDNs).
 Defaults to publicly cached for 6 months.
 
 

+ 2 - 1
docs/docs/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy.md

@@ -151,7 +151,8 @@ Using type `any` in order to avoid the need to include `aws-sdk` dependency in g
 
 <GenerationInfo sourceFile="packages/asset-server-plugin/src/s3-asset-storage-strategy.ts" sourceLine="119" packageName="@vendure/asset-server-plugin" />
 
-Returns a configured instance of the <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#s3assetstoragestrategy'>S3AssetStorageStrategy</a> which can then be passed to the <a href='/reference/core-plugins/asset-server-plugin/asset-server-options#assetserveroptions'>AssetServerOptions</a>`storageStrategyFactory` property.
+Returns a configured instance of the <a href='/reference/core-plugins/asset-server-plugin/s3asset-storage-strategy#s3assetstoragestrategy'>S3AssetStorageStrategy</a> which can then be passed to the <a href='/reference/core-plugins/asset-server-plugin/asset-server-options#assetserveroptions'>AssetServerOptions</a>
+`storageStrategyFactory` property.
 
 Before using this strategy, make sure you have the `@aws-sdk/client-s3` and `@aws-sdk/lib-storage` package installed:
 

+ 114 - 114
docs/docs/reference/core-plugins/elasticsearch-plugin/elasticsearch-options.md

@@ -24,23 +24,23 @@ interface ElasticsearchOptions {
     clientOptions?: ClientOptions;
     indexPrefix?: string;
     indexSettings?: object;
-    indexMappingProperties?: {
-        [indexName: string]: object;
+    indexMappingProperties?: {
+        [indexName: string]: object;
     };
     reindexProductsChunkSize?: number;
     reindexBulkOperationSizeLimit?: number;
     searchConfig?: SearchConfig;
-    customProductMappings?: {
-        [fieldName: string]: CustomMapping<[Product, ProductVariant[], LanguageCode, Injector, RequestContext]>;
+    customProductMappings?: {
+        [fieldName: string]: CustomMapping<[Product, ProductVariant[], LanguageCode, Injector, RequestContext]>;
     };
-    customProductVariantMappings?: {
-        [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode, Injector, RequestContext]>;
+    customProductVariantMappings?: {
+        [fieldName: string]: CustomMapping<[ProductVariant, LanguageCode, Injector, RequestContext]>;
     };
     bufferUpdates?: boolean;
     hydrateProductRelations?: Array<EntityRelationPaths<Product>>;
     hydrateProductVariantRelations?: Array<EntityRelationPaths<ProductVariant>>;
-    extendSearchInputType?: {
-        [name: string]: PrimitiveTypeVariations<GraphQlPrimitive>;
+    extendSearchInputType?: {
+        [name: string]: PrimitiveTypeVariations<GraphQlPrimitive>;
     };
     extendSearchSortType?: string[];
 }
@@ -72,9 +72,9 @@ Interval in milliseconds between attempts to connect to the ElasticSearch server
 
 <MemberInfo kind="property" type={`ClientOptions`}   />
 
-Options to pass directly to the
-[Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
-set authentication or other more advanced options.
+Options to pass directly to the
+[Elasticsearch Node.js client](https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/index.html). For example, to
+set authentication or other more advanced options.
 Note that if the `node` or `nodes` option is specified, it will override the values provided in the `host` and `port` options.
 ### indexPrefix
 
@@ -85,7 +85,7 @@ Prefix for the indices created by the plugin.
 
 <MemberInfo kind="property" type={`object`} default={`{}`}  since="1.2.0"  />
 
-[These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
+[These options](https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index-modules.html#index-modules-settings)
 are directly passed to index settings. To apply some settings indices will be recreated.
 
 *Example*
@@ -116,10 +116,10 @@ A more complete example can be found in the discussion thread
 [How to make elastic plugin to search by substring with stemming](https://github.com/vendure-ecommerce/vendure/discussions/1066).
 ### indexMappingProperties
 
-<MemberInfo kind="property" type={`{         [indexName: string]: object;     }`} default={`{}`}  since="1.2.0"  />
+<MemberInfo kind="property" type={`{
         [indexName: string]: object;
     }`} default={`{}`}  since="1.2.0"  />
 
-This option allow to redefine or define new properties in mapping. More about elastic
-[mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
+This option allow to redefine or define new properties in mapping. More about elastic
+[mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html)
 After changing this option indices will be recreated.
 
 *Example*
@@ -167,8 +167,8 @@ Products limit chunk size for each loop iteration when indexing products.
 
 <MemberInfo kind="property" type={`number`} default={`3000`}  since="2.1.7"  />
 
-Index operations are performed in bulk, with each bulk operation containing a number of individual
-index operations. This option sets the maximum number of operations in the memory buffer before a
+Index operations are performed in bulk, with each bulk operation containing a number of individual
+index operations. This option sets the maximum number of operations in the memory buffer before a
 bulk operation is executed.
 ### searchConfig
 
@@ -177,21 +177,21 @@ bulk operation is executed.
 Configuration of the internal Elasticsearch query.
 ### customProductMappings
 
-<MemberInfo kind="property" type={`{         [fieldName: string]: CustomMapping&#60;[<a href='/reference/typescript-api/entities/product#product'>Product</a>, <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>[], <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]&#62;;     }`}   />
-
-Custom mappings may be defined which will add the defined data to the
-Elasticsearch index and expose that data via the SearchResult GraphQL type,
-adding a new `customMappings`, `customProductMappings` & `customProductVariantMappings` fields.
-
-The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
-versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
-
-The `public` (default = `true`) property is used to reveal or hide the property in the GraphQL API schema.
-If this property is set to `false` it's not accessible in the `customMappings` field but it's still getting
-parsed to the elasticsearch index.
-
-This config option defines custom mappings which are accessible when the "groupByProduct"
-input options is set to `true`. In addition, custom variant mappings can be accessed by using
+<MemberInfo kind="property" type={`{
         [fieldName: string]: CustomMapping&#60;[<a href='/reference/typescript-api/entities/product#product'>Product</a>, <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>[], <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]&#62;;
     }`}   />
+
+Custom mappings may be defined which will add the defined data to the
+Elasticsearch index and expose that data via the SearchResult GraphQL type,
+adding a new `customMappings`, `customProductMappings` & `customProductVariantMappings` fields.
+
+The `graphQlType` property may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
+versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
+
+The `public` (default = `true`) property is used to reveal or hide the property in the GraphQL API schema.
+If this property is set to `false` it's not accessible in the `customMappings` field but it's still getting
+parsed to the elasticsearch index.
+
+This config option defines custom mappings which are accessible when the "groupByProduct"
+input options is set to `true`. In addition, custom variant mappings can be accessed by using
 the `customProductVariantMappings` field, which is always available.
 
 *Example*
@@ -240,10 +240,10 @@ query SearchProducts($input: SearchInput!) {
 ```
 ### customProductVariantMappings
 
-<MemberInfo kind="property" type={`{         [fieldName: string]: CustomMapping&#60;[<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>, <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]&#62;;     }`}   />
+<MemberInfo kind="property" type={`{
         [fieldName: string]: CustomMapping&#60;[<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>, <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>, <a href='/reference/typescript-api/common/injector#injector'>Injector</a>, <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>]&#62;;
     }`}   />
 
-This config option defines custom mappings which are accessible when the "groupByProduct"
-input options is set to `false`. In addition, custom product mappings can be accessed by using
+This config option defines custom mappings which are accessible when the "groupByProduct"
+input options is set to `false`. In addition, custom product mappings can be accessed by using
 the `customProductMappings` field, which is always available.
 
 *Example*
@@ -271,20 +271,20 @@ query SearchProducts($input: SearchInput!) {
 
 <MemberInfo kind="property" type={`boolean`} default={`false`}  since="1.3.0"  />
 
-If set to `true`, updates to Products, ProductVariants and Collections will not immediately
-trigger an update to the search index. Instead, all these changes will be buffered and will
-only be run via a call to the `runPendingSearchIndexUpdates` mutation in the Admin API.
-
-This is very useful for installations with a large number of ProductVariants and/or
-Collections, as the buffering allows better control over when these expensive jobs are run,
-and also performs optimizations to minimize the amount of work that needs to be performed by
+If set to `true`, updates to Products, ProductVariants and Collections will not immediately
+trigger an update to the search index. Instead, all these changes will be buffered and will
+only be run via a call to the `runPendingSearchIndexUpdates` mutation in the Admin API.
+
+This is very useful for installations with a large number of ProductVariants and/or
+Collections, as the buffering allows better control over when these expensive jobs are run,
+and also performs optimizations to minimize the amount of work that needs to be performed by
 the worker.
 ### hydrateProductRelations
 
 <MemberInfo kind="property" type={`Array&#60;<a href='/reference/typescript-api/common/entity-relation-paths#entityrelationpaths'>EntityRelationPaths</a>&#60;<a href='/reference/typescript-api/entities/product#product'>Product</a>&#62;&#62;`} default={`[]`}  since="1.3.0"  />
 
-Additional product relations that will be fetched from DB while reindexing. This can be used
-in combination with `customProductMappings` to ensure that the required relations are joined
+Additional product relations that will be fetched from DB while reindexing. This can be used
+in combination with `customProductMappings` to ensure that the required relations are joined
 before the `product` object is passed to the `valueFn`.
 
 *Example*
@@ -306,14 +306,14 @@ before the `product` object is passed to the `valueFn`.
 
 <MemberInfo kind="property" type={`Array&#60;<a href='/reference/typescript-api/common/entity-relation-paths#entityrelationpaths'>EntityRelationPaths</a>&#60;<a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a>&#62;&#62;`} default={`[]`}  since="1.3.0"  />
 
-Additional variant relations that will be fetched from DB while reindexing. See
+Additional variant relations that will be fetched from DB while reindexing. See
 `hydrateProductRelations` for more explanation and a usage example.
 ### extendSearchInputType
 
-<MemberInfo kind="property" type={`{         [name: string]: PrimitiveTypeVariations&#60;GraphQlPrimitive&#62;;     }`} default={`{}`}  since="1.3.0"  />
+<MemberInfo kind="property" type={`{
         [name: string]: PrimitiveTypeVariations&#60;GraphQlPrimitive&#62;;
     }`} default={`{}`}  since="1.3.0"  />
 
-Allows the `SearchInput` type to be extended with new input fields. This allows arbitrary
-data to be passed in, which can then be used e.g. in the `mapQuery()` function or
+Allows the `SearchInput` type to be extended with new input fields. This allows arbitrary
+data to be passed in, which can then be used e.g. in the `mapQuery()` function or
 custom `scriptFields` functions.
 
 *Example*
@@ -347,7 +347,7 @@ query {
 
 <MemberInfo kind="property" type={`string[]`} default={`[]`}  since="1.4.0"  />
 
-Adds a list of sort parameters. This is mostly important to make the
+Adds a list of sort parameters. This is mostly important to make the
 correct sort order values available inside `input` parameter of the `mapSort` option.
 
 *Example*
@@ -384,12 +384,12 @@ interface SearchConfig {
     multiMatchType?: 'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix';
     boostFields?: BoostFieldsConfig;
     priceRangeBucketInterval?: number;
-    mapQuery?: (
-        query: any,
-        input: ElasticSearchInput,
-        searchConfig: DeepRequired<SearchConfig>,
-        channelId: ID,
-        enabledOnly: boolean,
+    mapQuery?: (
+        query: any,
+        input: ElasticSearchInput,
+        searchConfig: DeepRequired<SearchConfig>,
+        channelId: ID,
+        enabledOnly: boolean,
     ) => any;
     scriptFields?: { [fieldName: string]: CustomScriptMapping<[ElasticSearchInput]> };
     mapSort?: (sort: ElasticSearchSortInput, input: ElasticSearchInput) => ElasticSearchSortInput;
@@ -402,29 +402,29 @@ interface SearchConfig {
 
 <MemberInfo kind="property" type={`number`} default={`50`}   />
 
-The maximum number of FacetValues to return from the search query. Internally, this
+The maximum number of FacetValues to return from the search query. Internally, this
 value sets the "size" property of an Elasticsearch aggregation.
 ### collectionMaxSize
 
 <MemberInfo kind="property" type={`number`} default={`50`}  since="1.1.0"  />
 
-The maximum number of Collections to return from the search query. Internally, this
+The maximum number of Collections to return from the search query. Internally, this
 value sets the "size" property of an Elasticsearch aggregation.
 ### totalItemsMaxSize
 
 <MemberInfo kind="property" type={`number | boolean`} default={`10000`}  since="1.2.0"  />
 
-The maximum number of totalItems to return from the search query. Internally, this
-value sets the "track_total_hits" property of an Elasticsearch query.
-If this parameter is set to "True", accurate count of totalItems will be returned.
-If this parameter is set to "False", totalItems will be returned as 0.
+The maximum number of totalItems to return from the search query. Internally, this
+value sets the "track_total_hits" property of an Elasticsearch query.
+If this parameter is set to "True", accurate count of totalItems will be returned.
+If this parameter is set to "False", totalItems will be returned as 0.
 If this parameter is set to integer, accurate count of totalItems will be returned not bigger than integer.
 ### multiMatchType
 
 <MemberInfo kind="property" type={`'best_fields' | 'most_fields' | 'cross_fields' | 'phrase' | 'phrase_prefix' | 'bool_prefix'`} default={`'best_fields'`}   />
 
-Defines the
-[multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
+Defines the
+[multi match type](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types)
 used when matching against a search term.
 ### boostFields
 
@@ -435,43 +435,43 @@ Set custom boost values for particular fields when matching against a search ter
 
 <MemberInfo kind="property" type={`number`}   />
 
-The interval used to group search results into buckets according to price range. For example, setting this to
-`2000` will group into buckets every $20.00:
-
-```json
-{
-  "data": {
-    "search": {
-      "totalItems": 32,
-      "priceRange": {
-        "buckets": [
-          {
-            "to": 2000,
-            "count": 21
-          },
-          {
-            "to": 4000,
-            "count": 7
-          },
-          {
-            "to": 6000,
-            "count": 3
-          },
-          {
-            "to": 12000,
-            "count": 1
-          }
-        ]
-      }
-    }
-  }
-}
+The interval used to group search results into buckets according to price range. For example, setting this to
+`2000` will group into buckets every $20.00:
+
+```json
+{
+  "data": {
+    "search": {
+      "totalItems": 32,
+      "priceRange": {
+        "buckets": [
+          {
+            "to": 2000,
+            "count": 21
+          },
+          {
+            "to": 4000,
+            "count": 7
+          },
+          {
+            "to": 6000,
+            "count": 3
+          },
+          {
+            "to": 12000,
+            "count": 1
+          }
+        ]
+      }
+    }
+  }
+}
 ```
 ### mapQuery
 
-<MemberInfo kind="property" type={`(         query: any,         input: ElasticSearchInput,         searchConfig: DeepRequired&#60;<a href='/reference/core-plugins/elasticsearch-plugin/elasticsearch-options#searchconfig'>SearchConfig</a>&#62;,         channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>,         enabledOnly: boolean,     ) =&#62; any`}   />
+<MemberInfo kind="property" type={`(
         query: any,
         input: ElasticSearchInput,
         searchConfig: DeepRequired&#60;<a href='/reference/core-plugins/elasticsearch-plugin/elasticsearch-options#searchconfig'>SearchConfig</a>&#62;,
         channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>,
         enabledOnly: boolean,
     ) =&#62; any`}   />
 
-This config option allows the the modification of the whole (already built) search query. This allows
+This config option allows the the modification of the whole (already built) search query. This allows
 for e.g. wildcard / fuzzy searches on the index.
 
 *Example*
@@ -510,17 +510,17 @@ mapQuery: (query, input, searchConfig, channelId, enabledOnly){
 
 <MemberInfo kind="property" type={`{ [fieldName: string]: CustomScriptMapping&#60;[ElasticSearchInput]&#62; }`}  since="1.3.0"  />
 
-Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
-
-The script field definition consists of three properties:
-
-* `graphQlType`: This is the type that will be returned when this script field is queried
-via the GraphQL API. It may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
-versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
-* `context`: determines whether this script field is available when grouping by product. Can be
-`product`, `variant` or `both`.
-* `scriptFn`: This is the function to run on each hit. Should return an object with a `script` property,
-as covered in the
+Sets `script_fields` inside the elasticsearch body which allows returning a script evaluation for each hit.
+
+The script field definition consists of three properties:
+
+* `graphQlType`: This is the type that will be returned when this script field is queried
+via the GraphQL API. It may be one of `String`, `Int`, `Float`, `Boolean`, `ID` or list
+versions thereof (`[String!]` etc) and can be appended with a `!` to indicate non-nullable fields.
+* `context`: determines whether this script field is available when grouping by product. Can be
+`product`, `variant` or `both`.
+* `scriptFn`: This is the function to run on each hit. Should return an object with a `script` property,
+as covered in the
 [Elasticsearch script fields docs](https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-fields.html#script-fields)
 
 *Example*
@@ -571,10 +571,10 @@ searchConfig: {
 
 <MemberInfo kind="property" type={`(sort: ElasticSearchSortInput, input: ElasticSearchInput) =&#62; ElasticSearchSortInput`} default={`{}`}  since="1.4.0"  />
 
-Allows extending the `sort` input of the elasticsearch body as covered in
-[Elasticsearch sort docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
-
-The `sort` input parameter contains the ElasticSearchSortInput generated for the default sort parameters "name" and "price".
+Allows extending the `sort` input of the elasticsearch body as covered in
+[Elasticsearch sort docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html)
+
+The `sort` input parameter contains the ElasticSearchSortInput generated for the default sort parameters "name" and "price".
 If neither of those are applied it will be empty.
 
 *Example*
@@ -655,9 +655,9 @@ searchConfig: {
 
 <GenerationInfo sourceFile="packages/elasticsearch-plugin/src/options.ts" sourceLine="680" packageName="@vendure/elasticsearch-plugin" />
 
-Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
-the scores of given fields when performing a search against a term.
-
+Configuration for [boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#field-boost)
+the scores of given fields when performing a search against a term.
+
 Boosting a field acts as a score multiplier for matches against that field.
 
 ```ts title="Signature"

+ 1 - 1
docs/docs/reference/core-plugins/elasticsearch-plugin/index.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ElasticsearchPlugin
 
-<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="223" packageName="@vendure/elasticsearch-plugin" />
+<GenerationInfo sourceFile="packages/elasticsearch-plugin/src/plugin.ts" sourceLine="224" packageName="@vendure/elasticsearch-plugin" />
 
 This plugin allows your product search to be powered by [Elasticsearch](https://github.com/elastic/elasticsearch) - a powerful Open Source search
 engine. This is a drop-in replacement for the DefaultSearchPlugin which exposes many powerful configuration options enabling your storefront

+ 23 - 23
docs/docs/reference/core-plugins/harden-plugin/harden-plugin-options.md

@@ -20,8 +20,8 @@ interface HardenPluginOptions {
     maxQueryComplexity?: number;
     queryComplexityEstimators?: ComplexityEstimator[];
     logComplexityScore?: boolean;
-    customComplexityFactors?: {
-        [path: string]: number;
+    customComplexityFactors?: {
+        [path: string]: number;
     };
     hideFieldSuggestions?: boolean;
     apiMode?: 'dev' | 'prod';
@@ -34,33 +34,33 @@ interface HardenPluginOptions {
 
 <MemberInfo kind="property" type={`number`} default={`1000`}   />
 
-Defines the maximum permitted complexity score of a query. The complexity score is based
-on the number of fields being selected as well as other factors like whether there are nested
-lists.
-
+Defines the maximum permitted complexity score of a query. The complexity score is based
+on the number of fields being selected as well as other factors like whether there are nested
+lists.
+
 A query which exceeds the maximum score will result in an error.
 ### queryComplexityEstimators
 
 <MemberInfo kind="property" type={`ComplexityEstimator[]`}   />
 
-An array of custom estimator functions for calculating the complexity of a query. By default,
-the plugin will use the <a href='/reference/core-plugins/harden-plugin/default-vendure-complexity-estimator#defaultvendurecomplexityestimator'>defaultVendureComplexityEstimator</a> which is specifically
+An array of custom estimator functions for calculating the complexity of a query. By default,
+the plugin will use the <a href='/reference/core-plugins/harden-plugin/default-vendure-complexity-estimator#defaultvendurecomplexityestimator'>defaultVendureComplexityEstimator</a> which is specifically
 tuned to accurately estimate Vendure queries.
 ### logComplexityScore
 
 <MemberInfo kind="property" type={`boolean`} default={`false`}   />
 
-When set to `true`, the complexity score of each query will be logged at the Verbose
-log level, and a breakdown of the calculation for each field will be logged at the Debug level.
-
+When set to `true`, the complexity score of each query will be logged at the Verbose
+log level, and a breakdown of the calculation for each field will be logged at the Debug level.
+
 This is very useful for tuning your complexity scores.
 ### customComplexityFactors
 
-<MemberInfo kind="property" type={`{         [path: string]: number;     }`}   />
+<MemberInfo kind="property" type={`{
         [path: string]: number;
     }`}   />
 
-This object allows you to tune the complexity weight of specific fields. For example,
-if you have a custom `stockLocations` field defined on the `ProductVariant` type, and
-you know that it is a particularly expensive operation to execute, you can increase
+This object allows you to tune the complexity weight of specific fields. For example,
+if you have a custom `stockLocations` field defined on the `ProductVariant` type, and
+you know that it is a particularly expensive operation to execute, you can increase
 its complexity like this:
 
 *Example*
@@ -77,19 +77,19 @@ HardenPlugin.init({
 
 <MemberInfo kind="property" type={`boolean`} default={`true`}   />
 
-Graphql-js will make suggestions about the names of fields if an invalid field name is provided.
-This would allow an attacker to find out the available fields by brute force even if introspection
-is disabled.
-
-Setting this option to `true` will prevent these suggestion error messages from being returned,
+Graphql-js will make suggestions about the names of fields if an invalid field name is provided.
+This would allow an attacker to find out the available fields by brute force even if introspection
+is disabled.
+
+Setting this option to `true` will prevent these suggestion error messages from being returned,
 instead replacing the message with a generic "Invalid request" message.
 ### apiMode
 
 <MemberInfo kind="property" type={`'dev' | 'prod'`} default={`'prod'`}   />
 
-When set to `'prod'`, the plugin will disable dev-mode features of the GraphQL APIs:
-
-- introspection
+When set to `'prod'`, the plugin will disable dev-mode features of the GraphQL APIs:
+
+- introspection
 - GraphQL playground
 
 

+ 116 - 116
docs/docs/reference/core-plugins/payments-plugin/mollie-plugin.md

@@ -13,110 +13,110 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/payments-plugin/src/mollie/mollie.plugin.ts" sourceLine="191" packageName="@vendure/payments-plugin" />
 
-Plugin to enable payments through the [Mollie platform](https://docs.mollie.com/).
-This plugin uses the Order API from Mollie, not the Payments API.
-
-## Requirements
-
-1. You will need to create a Mollie account and get your apiKey in the dashboard.
-2. Install the Payments plugin and the Mollie client:
-
-    `yarn add @vendure/payments-plugin @mollie/api-client`
-
-    or
-
-    `npm install @vendure/payments-plugin @mollie/api-client`
-
-## Setup
-
-1. Add the plugin to your VendureConfig `plugins` array:
-    ```ts
-    import { MolliePlugin } from '@vendure/payments-plugin/package/mollie';
-
-    // ...
-
-    plugins: [
-      MolliePlugin.init({ vendureHost: 'https://yourhost.io/' }),
-    ]
-    ```
-2. Run a database migration to add the `mollieOrderId` custom field to the order entity.
-3. Create a new PaymentMethod in the Admin UI, and select "Mollie payments" as the handler.
-4. Set your Mollie apiKey in the `API Key` field.
-5. Set the `Fallback redirectUrl` to the url that the customer should be redirected to after completing the payment.
-You can override this url by passing the `redirectUrl` as an argument to the `createMolliePaymentIntent` mutation.
-
-## Storefront usage
-
-In your storefront you add a payment to an order using the `createMolliePaymentIntent` mutation. In this example, our Mollie
-PaymentMethod was given the code "mollie-payment-method". The `redirectUrl``is the url that is used to redirect the end-user
-back to your storefront after completing the payment.
-
-```GraphQL
-mutation CreateMolliePaymentIntent {
-  createMolliePaymentIntent(input: {
-    redirectUrl: "https://storefront/order/1234XYZ"
-    paymentMethodCode: "mollie-payment-method"
-    molliePaymentMethodCode: "ideal"
-  }) {
-         ... on MolliePaymentIntent {
-              url
-          }
-         ... on MolliePaymentIntentError {
-              errorCode
-              message
-         }
-  }
-}
-```
-
-The response will contain
-a redirectUrl, which can be used to redirect your customer to the Mollie
-platform.
-
-'molliePaymentMethodCode' is an optional parameter that can be passed to skip Mollie's hosted payment method selection screen
-You can get available Mollie payment methods with the following query:
-
-```GraphQL
-{
- molliePaymentMethods(input: { paymentMethodCode: "mollie-payment-method" }) {
-   id
-   code
-   description
-   minimumAmount {
-     value
-     currency
-   }
-   maximumAmount {
-     value
-     currency
-   }
-   image {
-     size1x
-     size2x
-     svg
-   }
- }
-}
-```
-You can pass `creditcard` for example, to the `createMolliePaymentIntent` mutation to skip the method selection.
-
-After completing payment on the Mollie platform,
-the user is redirected to the given redirect url, e.g. `https://storefront/order/CH234X5`
-
-## Pay later methods
-Mollie supports pay-later methods like 'Klarna Pay Later'. For pay-later methods, the status of an order is
-'PaymentAuthorized' after the Mollie hosted checkout. You need to manually settle the payment via the admin ui to capture the payment!
-Make sure you capture a payment within 28 days, because this is the Klarna expiry time
-
-If you don't want this behaviour (Authorized first), you can set 'autoCapture=true' on the payment method. This option will immediately
-capture the payment after a customer authorizes the payment.
-
-## ArrangingAdditionalPayment state
-
-In some rare cases, a customer can add items to the active order, while a Mollie payment is still open,
-for example by opening your storefront in another browser tab.
-This could result in an order being in `ArrangingAdditionalPayment` status after the customer finished payment.
-You should check if there is still an active order with status `ArrangingAdditionalPayment` on your order confirmation page,
+Plugin to enable payments through the [Mollie platform](https://docs.mollie.com/).
+This plugin uses the Order API from Mollie, not the Payments API.
+
+## Requirements
+
+1. You will need to create a Mollie account and get your apiKey in the dashboard.
+2. Install the Payments plugin and the Mollie client:
+
+    `yarn add @vendure/payments-plugin @mollie/api-client`
+
+    or
+
+    `npm install @vendure/payments-plugin @mollie/api-client`
+
+## Setup
+
+1. Add the plugin to your VendureConfig `plugins` array:
+    ```ts
+    import { MolliePlugin } from '@vendure/payments-plugin/package/mollie';
+
+    // ...
+
+    plugins: [
+      MolliePlugin.init({ vendureHost: 'https://yourhost.io/' }),
+    ]
+    ```
+2. Run a database migration to add the `mollieOrderId` custom field to the order entity.
+3. Create a new PaymentMethod in the Admin UI, and select "Mollie payments" as the handler.
+4. Set your Mollie apiKey in the `API Key` field.
+5. Set the `Fallback redirectUrl` to the url that the customer should be redirected to after completing the payment.
+You can override this url by passing the `redirectUrl` as an argument to the `createMolliePaymentIntent` mutation.
+
+## Storefront usage
+
+In your storefront you add a payment to an order using the `createMolliePaymentIntent` mutation. In this example, our Mollie
+PaymentMethod was given the code "mollie-payment-method". The `redirectUrl``is the url that is used to redirect the end-user
+back to your storefront after completing the payment.
+
+```GraphQL
+mutation CreateMolliePaymentIntent {
+  createMolliePaymentIntent(input: {
+    redirectUrl: "https://storefront/order/1234XYZ"
+    paymentMethodCode: "mollie-payment-method"
+    molliePaymentMethodCode: "ideal"
+  }) {
+         ... on MolliePaymentIntent {
+              url
+          }
+         ... on MolliePaymentIntentError {
+              errorCode
+              message
+         }
+  }
+}
+```
+
+The response will contain
+a redirectUrl, which can be used to redirect your customer to the Mollie
+platform.
+
+'molliePaymentMethodCode' is an optional parameter that can be passed to skip Mollie's hosted payment method selection screen
+You can get available Mollie payment methods with the following query:
+
+```GraphQL
+{
+ molliePaymentMethods(input: { paymentMethodCode: "mollie-payment-method" }) {
+   id
+   code
+   description
+   minimumAmount {
+     value
+     currency
+   }
+   maximumAmount {
+     value
+     currency
+   }
+   image {
+     size1x
+     size2x
+     svg
+   }
+ }
+}
+```
+You can pass `creditcard` for example, to the `createMolliePaymentIntent` mutation to skip the method selection.
+
+After completing payment on the Mollie platform,
+the user is redirected to the given redirect url, e.g. `https://storefront/order/CH234X5`
+
+## Pay later methods
+Mollie supports pay-later methods like 'Klarna Pay Later'. For pay-later methods, the status of an order is
+'PaymentAuthorized' after the Mollie hosted checkout. You need to manually settle the payment via the admin ui to capture the payment!
+Make sure you capture a payment within 28 days, because this is the Klarna expiry time
+
+If you don't want this behaviour (Authorized first), you can set 'autoCapture=true' on the payment method. This option will immediately
+capture the payment after a customer authorizes the payment.
+
+## ArrangingAdditionalPayment state
+
+In some rare cases, a customer can add items to the active order, while a Mollie payment is still open,
+for example by opening your storefront in another browser tab.
+This could result in an order being in `ArrangingAdditionalPayment` status after the customer finished payment.
+You should check if there is still an active order with status `ArrangingAdditionalPayment` on your order confirmation page,
 and if so, allow your customer to pay for the additional items by creating another Mollie payment.
 
 ```ts title="Signature"
@@ -152,10 +152,10 @@ Configuration options for the Mollie payments plugin.
 ```ts title="Signature"
 interface MolliePluginOptions {
     vendureHost: string;
-    enabledPaymentMethodsParams?: (
-        injector: Injector,
-        ctx: RequestContext,
-        order: Order | null,
+    enabledPaymentMethodsParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order | null,
     ) => AdditionalEnabledPaymentMethodsParams | Promise<AdditionalEnabledPaymentMethodsParams>;
 }
 ```
@@ -166,18 +166,18 @@ interface MolliePluginOptions {
 
 <MemberInfo kind="property" type={`string`}   />
 
-The host of your Vendure server, e.g. `'https://my-vendure.io'`.
+The host of your Vendure server, e.g. `'https://my-vendure.io'`.
 This is used by Mollie to send webhook events to the Vendure server
 ### enabledPaymentMethodsParams
 
-<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a> | null,     ) =&#62; AdditionalEnabledPaymentMethodsParams | Promise&#60;AdditionalEnabledPaymentMethodsParams&#62;`}  since="2.2.0"  />
-
-Provide additional parameters to the Mollie enabled payment methods API call. By default,
-the plugin will already pass the `resource` parameter.
-
-For example, if you want to provide a `locale` and `billingCountry` for the API call, you can do so like this:
+<MemberInfo kind="property" type={`(
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
         order: <a href='/reference/typescript-api/entities/order#order'>Order</a> | null,
     ) =&#62; AdditionalEnabledPaymentMethodsParams | Promise&#60;AdditionalEnabledPaymentMethodsParams&#62;`}  since="2.2.0"  />
 
-**Note:** The `order` argument is possibly `null`, this could happen when you fetch the available payment methods
+Provide additional parameters to the Mollie enabled payment methods API call. By default,
+the plugin will already pass the `resource` parameter.
+
+For example, if you want to provide a `locale` and `billingCountry` for the API call, you can do so like this:
+
+**Note:** The `order` argument is possibly `null`, this could happen when you fetch the available payment methods
 before the order is created.
 
 *Example*

+ 199 - 166
docs/docs/reference/core-plugins/payments-plugin/stripe-plugin.md

@@ -13,147 +13,147 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/payments-plugin/src/stripe/stripe.plugin.ts" sourceLine="161" packageName="@vendure/payments-plugin" />
 
-Plugin to enable payments through [Stripe](https://stripe.com/docs) via the Payment Intents API.
-
-## Requirements
-
-1. You will need to create a Stripe account and get your secret key in the dashboard.
-2. Create a webhook endpoint in the Stripe dashboard (Developers -> Webhooks, "Add an endpoint") which listens to the `payment_intent.succeeded`
-and `payment_intent.payment_failed` events. The URL should be `https://my-server.com/payments/stripe`, where
-`my-server.com` is the host of your Vendure server. *Note:* for local development, you'll need to use
-the Stripe CLI to test your webhook locally. See the _local development_ section below.
-3. Get the signing secret for the newly created webhook.
-4. Install the Payments plugin and the Stripe Node library:
-
-    `yarn add @vendure/payments-plugin stripe`
-
-    or
-
-    `npm install @vendure/payments-plugin stripe`
-
-## Setup
-
-1. Add the plugin to your VendureConfig `plugins` array:
-    ```ts
-    import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
-
-    // ...
-
-    plugins: [
-      StripePlugin.init({
-        // This prevents different customers from using the same PaymentIntent
-        storeCustomersInStripe: true,
-      }),
-    ]
-    ````
-    For all the plugin options, see the <a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripepluginoptions'>StripePluginOptions</a> type.
-2. Create a new PaymentMethod in the Admin UI, and select "Stripe payments" as the handler.
-3. Set the webhook secret and API key in the PaymentMethod form.
-
-## Storefront usage
-
-The plugin is designed to work with the [Custom payment flow](https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements).
-In this flow, Stripe provides libraries which handle the payment UI and confirmation for you. You can install it in your storefront project
-with:
-
-```shell
-yarn add @stripe/stripe-js
-# or
-npm install @stripe/stripe-js
-```
-
-If you are using React, you should also consider installing `@stripe/react-stripe-js`, which is a wrapper around Stripe Elements.
-
-The high-level workflow is:
-1. Create a "payment intent" on the server by executing the `createStripePaymentIntent` mutation which is exposed by this plugin.
-2. Use the returned client secret to instantiate the Stripe Payment Element:
-   ```ts
-   import { Elements } from '@stripe/react-stripe-js';
-   import { loadStripe, Stripe } from '@stripe/stripe-js';
-   import { CheckoutForm } from './CheckoutForm';
-
-   const stripePromise = getStripe('pk_test_....wr83u');
-
-   type StripePaymentsProps = {
-     clientSecret: string;
-     orderCode: string;
-   }
-
-   export function StripePayments({ clientSecret, orderCode }: StripePaymentsProps) {
-     const options = {
-       // passing the client secret obtained from the server
-       clientSecret,
-     }
-     return (
-       <Elements stripe={stripePromise} options={options}>
-         <CheckoutForm orderCode={orderCode} />
-       </Elements>
-     );
-   }
-   ```
-   ```ts
-   // CheckoutForm.tsx
-   import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
-   import { FormEvent } from 'react';
-
-   export const CheckoutForm = ({ orderCode }: { orderCode: string }) => {
-     const stripe = useStripe();
-     const elements = useElements();
-
-     const handleSubmit = async (event: FormEvent) => {
-       // We don't want to let default form submission happen here,
-       // which would refresh the page.
-       event.preventDefault();
-
-       if (!stripe || !elements) {
-         // Stripe.js has not yet loaded.
-         // Make sure to disable form submission until Stripe.js has loaded.
-         return;
-       }
-
-       const result = await stripe.confirmPayment({
-         //`Elements` instance that was used to create the Payment Element
-         elements,
-         confirmParams: {
-           return_url: location.origin + `/checkout/confirmation/${orderCode}`,
-         },
-       });
-
-       if (result.error) {
-         // Show error to your customer (for example, payment details incomplete)
-         console.log(result.error.message);
-       } else {
-         // Your customer will be redirected to your `return_url`. For some payment
-         // methods like iDEAL, your customer will be redirected to an intermediate
-         // site first to authorize the payment, then redirected to the `return_url`.
-       }
-     };
-
-     return (
-       <form onSubmit={handleSubmit}>
-         <PaymentElement />
-         <button disabled={!stripe}>Submit</button>
-       </form>
-     );
-   };
-   ```
-3. Once the form is submitted and Stripe processes the payment, the webhook takes care of updating the order without additional action
-in the storefront. As in the code above, the customer will be redirected to `/checkout/confirmation/${orderCode}`.
-
-:::info
-A full working storefront example of the Stripe integration can be found in the
-[Remix Starter repo](https://github.com/vendure-ecommerce/storefront-remix-starter/tree/master/app/components/checkout/stripe)
-:::
-
-## Local development
-
-1. Download & install the Stripe CLI: https://stripe.com/docs/stripe-cli
-2. From your Stripe dashboard, go to Developers -> Webhooks and click "Add an endpoint" and follow the instructions
-under "Test in a local environment".
-3. The Stripe CLI command will look like
-   ```shell
-   stripe listen --forward-to localhost:3000/payments/stripe
-   ```
+Plugin to enable payments through [Stripe](https://stripe.com/docs) via the Payment Intents API.
+
+## Requirements
+
+1. You will need to create a Stripe account and get your secret key in the dashboard.
+2. Create a webhook endpoint in the Stripe dashboard (Developers -> Webhooks, "Add an endpoint") which listens to the `payment_intent.succeeded`
+and `payment_intent.payment_failed` events. The URL should be `https://my-server.com/payments/stripe`, where
+`my-server.com` is the host of your Vendure server. *Note:* for local development, you'll need to use
+the Stripe CLI to test your webhook locally. See the _local development_ section below.
+3. Get the signing secret for the newly created webhook.
+4. Install the Payments plugin and the Stripe Node library:
+
+    `yarn add @vendure/payments-plugin stripe`
+
+    or
+
+    `npm install @vendure/payments-plugin stripe`
+
+## Setup
+
+1. Add the plugin to your VendureConfig `plugins` array:
+    ```ts
+    import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+    // ...
+
+    plugins: [
+      StripePlugin.init({
+        // This prevents different customers from using the same PaymentIntent
+        storeCustomersInStripe: true,
+      }),
+    ]
+    ````
+    For all the plugin options, see the <a href='/reference/core-plugins/payments-plugin/stripe-plugin#stripepluginoptions'>StripePluginOptions</a> type.
+2. Create a new PaymentMethod in the Admin UI, and select "Stripe payments" as the handler.
+3. Set the webhook secret and API key in the PaymentMethod form.
+
+## Storefront usage
+
+The plugin is designed to work with the [Custom payment flow](https://stripe.com/docs/payments/accept-a-payment?platform=web&ui=elements).
+In this flow, Stripe provides libraries which handle the payment UI and confirmation for you. You can install it in your storefront project
+with:
+
+```shell
+yarn add @stripe/stripe-js
+# or
+npm install @stripe/stripe-js
+```
+
+If you are using React, you should also consider installing `@stripe/react-stripe-js`, which is a wrapper around Stripe Elements.
+
+The high-level workflow is:
+1. Create a "payment intent" on the server by executing the `createStripePaymentIntent` mutation which is exposed by this plugin.
+2. Use the returned client secret to instantiate the Stripe Payment Element:
+   ```ts
+   import { Elements } from '@stripe/react-stripe-js';
+   import { loadStripe, Stripe } from '@stripe/stripe-js';
+   import { CheckoutForm } from './CheckoutForm';
+
+   const stripePromise = getStripe('pk_test_....wr83u');
+
+   type StripePaymentsProps = {
+     clientSecret: string;
+     orderCode: string;
+   }
+
+   export function StripePayments({ clientSecret, orderCode }: StripePaymentsProps) {
+     const options = {
+       // passing the client secret obtained from the server
+       clientSecret,
+     }
+     return (
+       <Elements stripe={stripePromise} options={options}>
+         <CheckoutForm orderCode={orderCode} />
+       </Elements>
+     );
+   }
+   ```
+   ```ts
+   // CheckoutForm.tsx
+   import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';
+   import { FormEvent } from 'react';
+
+   export const CheckoutForm = ({ orderCode }: { orderCode: string }) => {
+     const stripe = useStripe();
+     const elements = useElements();
+
+     const handleSubmit = async (event: FormEvent) => {
+       // We don't want to let default form submission happen here,
+       // which would refresh the page.
+       event.preventDefault();
+
+       if (!stripe || !elements) {
+         // Stripe.js has not yet loaded.
+         // Make sure to disable form submission until Stripe.js has loaded.
+         return;
+       }
+
+       const result = await stripe.confirmPayment({
+         //`Elements` instance that was used to create the Payment Element
+         elements,
+         confirmParams: {
+           return_url: location.origin + `/checkout/confirmation/${orderCode}`,
+         },
+       });
+
+       if (result.error) {
+         // Show error to your customer (for example, payment details incomplete)
+         console.log(result.error.message);
+       } else {
+         // Your customer will be redirected to your `return_url`. For some payment
+         // methods like iDEAL, your customer will be redirected to an intermediate
+         // site first to authorize the payment, then redirected to the `return_url`.
+       }
+     };
+
+     return (
+       <form onSubmit={handleSubmit}>
+         <PaymentElement />
+         <button disabled={!stripe}>Submit</button>
+       </form>
+     );
+   };
+   ```
+3. Once the form is submitted and Stripe processes the payment, the webhook takes care of updating the order without additional action
+in the storefront. As in the code above, the customer will be redirected to `/checkout/confirmation/${orderCode}`.
+
+:::info
+A full working storefront example of the Stripe integration can be found in the
+[Remix Starter repo](https://github.com/vendure-ecommerce/storefront-remix-starter/tree/master/app/components/checkout/stripe)
+:::
+
+## Local development
+
+1. Download & install the Stripe CLI: https://stripe.com/docs/stripe-cli
+2. From your Stripe dashboard, go to Developers -> Webhooks and click "Add an endpoint" and follow the instructions
+under "Test in a local environment".
+3. The Stripe CLI command will look like
+   ```shell
+   stripe listen --forward-to localhost:3000/payments/stripe
+   ```
 4. The Stripe CLI will create a webhook signing secret you can then use in your config of the StripePlugin.
 
 ```ts title="Signature"
@@ -182,27 +182,32 @@ Initialize the Stripe payment plugin
 
 ## StripePluginOptions
 
-<GenerationInfo sourceFile="packages/payments-plugin/src/stripe/types.ts" sourceLine="27" packageName="@vendure/payments-plugin" />
+<GenerationInfo sourceFile="packages/payments-plugin/src/stripe/types.ts" sourceLine="29" packageName="@vendure/payments-plugin" />
 
 Configuration options for the Stripe payments plugin.
 
 ```ts title="Signature"
 interface StripePluginOptions {
     storeCustomersInStripe?: boolean;
-    metadata?: (
-        injector: Injector,
-        ctx: RequestContext,
-        order: Order,
+    metadata?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
     ) => Stripe.MetadataParam | Promise<Stripe.MetadataParam>;
-    paymentIntentCreateParams?: (
-        injector: Injector,
-        ctx: RequestContext,
-        order: Order,
+    paymentIntentCreateParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
     ) => AdditionalPaymentIntentCreateParams | Promise<AdditionalPaymentIntentCreateParams>;
-    customerCreateParams?: (
-        injector: Injector,
-        ctx: RequestContext,
-        order: Order,
+    requestOptions?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => AdditionalRequestOptions | Promise<AdditionalRequestOptions>;
+    customerCreateParams?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
     ) => AdditionalCustomerCreateParams | Promise<AdditionalCustomerCreateParams>;
 }
 ```
@@ -213,13 +218,13 @@ interface StripePluginOptions {
 
 <MemberInfo kind="property" type={`boolean`} default={`false`}   />
 
-If set to `true`, a [Customer](https://stripe.com/docs/api/customers) object will be created in Stripe - if
-it doesn't already exist - for authenticated users, which prevents payment methods attached to other Customers
-to be used with the same PaymentIntent. This is done by adding a custom field to the Customer entity to store
+If set to `true`, a [Customer](https://stripe.com/docs/api/customers) object will be created in Stripe - if
+it doesn't already exist - for authenticated users, which prevents payment methods attached to other Customers
+to be used with the same PaymentIntent. This is done by adding a custom field to the Customer entity to store
 the Stripe customer ID, so switching this on will require a database migration / synchronization.
 ### metadata
 
-<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; Stripe.MetadataParam | Promise&#60;Stripe.MetadataParam&#62;`}  since="1.9.7"  />
+<MemberInfo kind="property" type={`(
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,
     ) =&#62; Stripe.MetadataParam | Promise&#60;Stripe.MetadataParam&#62;`}  since="1.9.7"  />
 
 Attach extra metadata to Stripe payment intent creation call.
 
@@ -249,11 +254,11 @@ Note: If the `paymentIntentCreateParams` is also used and returns a `metadata` k
 returned by both functions will be merged.
 ### paymentIntentCreateParams
 
-<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; AdditionalPaymentIntentCreateParams | Promise&#60;AdditionalPaymentIntentCreateParams&#62;`}  since="2.1.0"  />
-
-Provide additional parameters to the Stripe payment intent creation. By default,
-the plugin will already pass the `amount`, `currency`, `customer` and `automatic_payment_methods: { enabled: true }` parameters.
+<MemberInfo kind="property" type={`(
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,
     ) =&#62; AdditionalPaymentIntentCreateParams | Promise&#60;AdditionalPaymentIntentCreateParams&#62;`}  since="2.1.0"  />
 
+Provide additional parameters to the Stripe payment intent creation. By default,
+the plugin will already pass the `amount`, `currency`, `customer` and `automatic_payment_methods: { enabled: true }` parameters.
+
 For example, if you want to provide a `description` for the payment intent, you can do so like this:
 
 *Example*
@@ -275,13 +280,41 @@ export const config: VendureConfig = {
   ],
 };
 ```
-### customerCreateParams
+### requestOptions
+
+<MemberInfo kind="property" type={`(
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,
     ) =&#62; AdditionalRequestOptions | Promise&#60;AdditionalRequestOptions&#62;`}  since="3.1.0"  />
+
+Provide additional options to the Stripe payment intent creation. By default,
+the plugin will already pass the `idempotencyKey` parameter.
+
+For example, if you want to provide a `stripeAccount` for the payment intent, you can do so like this:
 
-<MemberInfo kind="property" type={`(         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,     ) =&#62; AdditionalCustomerCreateParams | Promise&#60;AdditionalCustomerCreateParams&#62;`}  since="2.1.0"  />
+*Example*
+
+```ts
+import { VendureConfig } from '@vendure/core';
+import { StripePlugin } from '@vendure/payments-plugin/package/stripe';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    StripePlugin.init({
+      requestOptions: (injector, ctx, order) => {
+        return {
+          stripeAccount: ctx.channel.seller?.customFields.connectedAccountId
+        },
+      }
+    }),
+  ],
+};
+```
+### customerCreateParams
 
-Provide additional parameters to the Stripe customer creation. By default,
-the plugin will already pass the `email` and `name` parameters.
+<MemberInfo kind="property" type={`(
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>,
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>,
         order: <a href='/reference/typescript-api/entities/order#order'>Order</a>,
     ) =&#62; AdditionalCustomerCreateParams | Promise&#60;AdditionalCustomerCreateParams&#62;`}  since="2.1.0"  />
 
+Provide additional parameters to the Stripe customer creation. By default,
+the plugin will already pass the `email` and `name` parameters.
+
 For example, if you want to provide an address for the customer:
 
 *Example*

+ 9 - 9
docs/docs/reference/core-plugins/stellate-plugin/purge-rule.md

@@ -13,8 +13,8 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/stellate-plugin/src/purge-rule.ts" sourceLine="49" packageName="@vendure/stellate-plugin" />
 
-Defines a rule that listens for a particular VendureEvent and uses that to
-make calls to the [Stellate Purging API](https://docs.stellate.co/docs/purging-api) via
+Defines a rule that listens for a particular VendureEvent and uses that to
+make calls to the [Stellate Purging API](https://docs.stellate.co/docs/purging-api) via
 the provided <a href='/reference/core-plugins/stellate-plugin/stellate-service#stellateservice'>StellateService</a> instance.
 
 ```ts title="Signature"
@@ -63,10 +63,10 @@ Configures a <a href='/reference/core-plugins/stellate-plugin/purge-rule#purgeru
 interface PurgeRuleConfig<Event extends VendureEvent> {
     eventType: Type<Event>;
     bufferTime?: number;
-    handler: (handlerArgs: {
-        events: Event[];
-        stellateService: StellateService;
-        injector: Injector;
+    handler: (handlerArgs: {
+        events: Event[];
+        stellateService: StellateService;
+        injector: Injector;
     }) => void | Promise<void>;
 }
 ```
@@ -82,13 +82,13 @@ Specifies which VendureEvent will trigger this purge rule.
 
 <MemberInfo kind="property" type={`number`} default={`5000`}   />
 
-How long to buffer events for in milliseconds before executing the handler. This allows
+How long to buffer events for in milliseconds before executing the handler. This allows
 us to efficiently batch calls to the Stellate Purge API.
 ### handler
 
-<MemberInfo kind="property" type={`(handlerArgs: {         events: Event[];         stellateService: <a href='/reference/core-plugins/stellate-plugin/stellate-service#stellateservice'>StellateService</a>;         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>;     }) =&#62; void | Promise&#60;void&#62;`}   />
+<MemberInfo kind="property" type={`(handlerArgs: {
         events: Event[];
         stellateService: <a href='/reference/core-plugins/stellate-plugin/stellate-service#stellateservice'>StellateService</a>;
         injector: <a href='/reference/typescript-api/common/injector#injector'>Injector</a>;
     }) =&#62; void | Promise&#60;void&#62;`}   />
 
-The function to invoke when the specified event is published. This function should use the
+The function to invoke when the specified event is published. This function should use the
 <a href='/reference/core-plugins/stellate-plugin/stellate-service#stellateservice'>StellateService</a> instance to call the Stellate Purge API.
 
 

+ 1 - 0
docs/docs/reference/graphql-api/admin/enums.md

@@ -423,6 +423,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">ORDER_MODIFICATION_ERROR</div>
 <div class="graphql-code-line ">INELIGIBLE_SHIPPING_METHOD_ERROR</div>
 <div class="graphql-code-line ">NO_ACTIVE_ORDER_ERROR</div>
+<div class="graphql-code-line ">ORDER_INTERCEPTOR_ERROR</div>
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 

+ 5 - 9
docs/docs/reference/graphql-api/admin/input-types.md

@@ -2917,20 +2917,16 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-block">
 <div class="graphql-code-line top-level">input <span class="graphql-code-identifier">RefundOrderInput</span> &#123;</div>
-<div class="graphql-code-line ">lines: [<a href="/reference/graphql-api/admin/input-types#orderlineinput">OrderLineInput</a>!]!</div>
+<div class="graphql-code-line ">lines: [<a href="/reference/graphql-api/admin/input-types#orderlineinput">OrderLineInput</a>!]</div>
 
-<div class="graphql-code-line ">shipping: <a href="/reference/graphql-api/admin/object-types#money">Money</a>!</div>
+<div class="graphql-code-line ">shipping: <a href="/reference/graphql-api/admin/object-types#money">Money</a></div>
 
-<div class="graphql-code-line ">adjustment: <a href="/reference/graphql-api/admin/object-types#money">Money</a>!</div>
+<div class="graphql-code-line ">adjustment: <a href="/reference/graphql-api/admin/object-types#money">Money</a></div>
 
 <div class="graphql-code-line comment">"""</div>
-<div class="graphql-code-line comment">If an amount is specified, this value will be used to create a Refund rather than calculating the</div>
-
-<div class="graphql-code-line comment">amount automatically. This was added in v2.2 and will be the preferred way to specify the refund</div>
-
-<div class="graphql-code-line comment">amount in the future. The `lines`, <code>shipping</code> and <code>adjustment</code> fields will likely be removed in a future</div>
+<div class="graphql-code-line comment">The amount to be refunded to this particular payment. This was introduced in v2.2.0 as the preferred way to specify the refund amount.</div>
 
-<div class="graphql-code-line comment">version.</div>
+<div class="graphql-code-line comment">Can be as much as the total amount of the payment minus the sum of all previous refunds.</div>
 <div class="graphql-code-line comment">"""</div>
 <div class="graphql-code-line ">amount: <a href="/reference/graphql-api/admin/object-types#money">Money</a></div>
 

+ 24 - 0
docs/docs/reference/graphql-api/admin/mutations.md

@@ -1538,6 +1538,30 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">transitionPaymentToState(id: <a href="/reference/graphql-api/admin/object-types#id">ID</a>!, state: <a href="/reference/graphql-api/admin/object-types#string">String</a>!): <a href="/reference/graphql-api/admin/object-types#transitionpaymenttostateresult">TransitionPaymentToStateResult</a>!</div>
 
 
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## unsetDraftOrderBillingAddress
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level comment">Unsets the billing address for a draft Order</div>
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level">type <span class="graphql-code-identifier">Mutation</span> &#123;</div>
+<div class="graphql-code-line ">unsetDraftOrderBillingAddress(orderId: <a href="/reference/graphql-api/admin/object-types#id">ID</a>!): <a href="/reference/graphql-api/admin/object-types#order">Order</a>!</div>
+
+
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## unsetDraftOrderShippingAddress
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level comment">Unsets the shipping address for a draft Order</div>
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level">type <span class="graphql-code-identifier">Mutation</span> &#123;</div>
+<div class="graphql-code-line ">unsetDraftOrderShippingAddress(orderId: <a href="/reference/graphql-api/admin/object-types#id">ID</a>!): <a href="/reference/graphql-api/admin/object-types#order">Order</a>!</div>
+
+
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 

+ 19 - 14
docs/docs/reference/graphql-api/admin/object-types.md

@@ -284,8 +284,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/admin/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">ui: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
 
 
@@ -1034,8 +1032,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/admin/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/admin/object-types#string">String</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/admin/object-types#string">String</a></div>
@@ -1382,8 +1378,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/admin/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/admin/object-types#float">Float</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/admin/object-types#float">Float</a></div>
@@ -1675,8 +1669,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/admin/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/admin/object-types#int">Int</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/admin/object-types#int">Int</a></div>
@@ -2251,6 +2243,23 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">customFields: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
 
 
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## OrderInterceptorError
+
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level comment">Returned when an order operation is rejected by an OrderInterceptor method.</div>
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level">type <span class="graphql-code-identifier">OrderInterceptorError</span> &#123;</div>
+<div class="graphql-code-line ">errorCode: <a href="/reference/graphql-api/admin/enums#errorcode">ErrorCode</a>!</div>
+
+<div class="graphql-code-line ">message: <a href="/reference/graphql-api/admin/object-types#string">String</a>!</div>
+
+<div class="graphql-code-line ">interceptorError: <a href="/reference/graphql-api/admin/object-types#string">String</a>!</div>
+
+
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 
@@ -3406,7 +3415,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-block">
 <div class="graphql-code-line top-level">union <span class="graphql-code-identifier">RemoveOrderItemsResult</span> =</div>
-<div class="graphql-code-line "><a href="/reference/graphql-api/admin/object-types#order">Order</a> | <a href="/reference/graphql-api/admin/object-types#ordermodificationerror">OrderModificationError</a></div>
+<div class="graphql-code-line "><a href="/reference/graphql-api/admin/object-types#order">Order</a> | <a href="/reference/graphql-api/admin/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/admin/object-types#orderinterceptorerror">OrderInterceptorError</a></div>
 </div>
 
 ## Return
@@ -3976,8 +3985,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">length: <a href="/reference/graphql-api/admin/object-types#int">Int</a></div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">pattern: <a href="/reference/graphql-api/admin/object-types#string">String</a></div>
 
 <div class="graphql-code-line ">options: [<a href="/reference/graphql-api/admin/object-types#stringfieldoption">StringFieldOption</a>!]</div>
@@ -4244,8 +4251,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/admin/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/admin/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">ui: <a href="/reference/graphql-api/admin/object-types#json">JSON</a></div>
 
 
@@ -4298,7 +4303,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-block">
 <div class="graphql-code-line top-level">union <span class="graphql-code-identifier">UpdateOrderItemsResult</span> =</div>
-<div class="graphql-code-line "><a href="/reference/graphql-api/admin/object-types#order">Order</a> | <a href="/reference/graphql-api/admin/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/admin/object-types#orderlimiterror">OrderLimitError</a> | <a href="/reference/graphql-api/admin/object-types#negativequantityerror">NegativeQuantityError</a> | <a href="/reference/graphql-api/admin/object-types#insufficientstockerror">InsufficientStockError</a></div>
+<div class="graphql-code-line "><a href="/reference/graphql-api/admin/object-types#order">Order</a> | <a href="/reference/graphql-api/admin/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/admin/object-types#orderlimiterror">OrderLimitError</a> | <a href="/reference/graphql-api/admin/object-types#negativequantityerror">NegativeQuantityError</a> | <a href="/reference/graphql-api/admin/object-types#insufficientstockerror">InsufficientStockError</a> | <a href="/reference/graphql-api/admin/object-types#orderinterceptorerror">OrderInterceptorError</a></div>
 </div>
 
 ## UpdatePromotionResult

+ 1 - 0
docs/docs/reference/graphql-api/shop/enums.md

@@ -393,6 +393,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">ORDER_MODIFICATION_ERROR</div>
 <div class="graphql-code-line ">INELIGIBLE_SHIPPING_METHOD_ERROR</div>
 <div class="graphql-code-line ">NO_ACTIVE_ORDER_ERROR</div>
+<div class="graphql-code-line ">ORDER_INTERCEPTOR_ERROR</div>
 <div class="graphql-code-line ">ORDER_PAYMENT_STATE_ERROR</div>
 <div class="graphql-code-line ">INELIGIBLE_PAYMENT_METHOD_ERROR</div>
 <div class="graphql-code-line ">PAYMENT_FAILED_ERROR</div>

+ 19 - 14
docs/docs/reference/graphql-api/shop/object-types.md

@@ -224,8 +224,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/shop/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">ui: <a href="/reference/graphql-api/shop/object-types#json">JSON</a></div>
 
 
@@ -737,8 +735,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/shop/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/shop/object-types#string">String</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/shop/object-types#string">String</a></div>
@@ -989,8 +985,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/shop/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/shop/object-types#float">Float</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/shop/object-types#float">Float</a></div>
@@ -1245,8 +1239,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/shop/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">min: <a href="/reference/graphql-api/shop/object-types#int">Int</a></div>
 
 <div class="graphql-code-line ">max: <a href="/reference/graphql-api/shop/object-types#int">Int</a></div>
@@ -1594,6 +1586,23 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <div class="graphql-code-line ">customFields: <a href="/reference/graphql-api/shop/object-types#json">JSON</a></div>
 
 
+<div class="graphql-code-line top-level">&#125;</div>
+</div>
+
+## OrderInterceptorError
+
+<div class="graphql-code-block">
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level comment">Returned when an order operation is rejected by an OrderInterceptor method.</div>
+<div class="graphql-code-line top-level comment">"""</div>
+<div class="graphql-code-line top-level">type <span class="graphql-code-identifier">OrderInterceptorError</span> &#123;</div>
+<div class="graphql-code-line ">errorCode: <a href="/reference/graphql-api/shop/enums#errorcode">ErrorCode</a>!</div>
+
+<div class="graphql-code-line ">message: <a href="/reference/graphql-api/shop/object-types#string">String</a>!</div>
+
+<div class="graphql-code-line ">interceptorError: <a href="/reference/graphql-api/shop/object-types#string">String</a>!</div>
+
+
 <div class="graphql-code-line top-level">&#125;</div>
 </div>
 
@@ -2544,7 +2553,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-block">
 <div class="graphql-code-line top-level">union <span class="graphql-code-identifier">RemoveOrderItemsResult</span> =</div>
-<div class="graphql-code-line "><a href="/reference/graphql-api/shop/object-types#order">Order</a> | <a href="/reference/graphql-api/shop/object-types#ordermodificationerror">OrderModificationError</a></div>
+<div class="graphql-code-line "><a href="/reference/graphql-api/shop/object-types#order">Order</a> | <a href="/reference/graphql-api/shop/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/shop/object-types#orderinterceptorerror">OrderInterceptorError</a></div>
 </div>
 
 ## RequestPasswordResetResult
@@ -2927,8 +2936,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">length: <a href="/reference/graphql-api/shop/object-types#int">Int</a></div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">pattern: <a href="/reference/graphql-api/shop/object-types#string">String</a></div>
 
 <div class="graphql-code-line ">options: [<a href="/reference/graphql-api/shop/object-types#stringfieldoption">StringFieldOption</a>!]</div>
@@ -3157,8 +3164,6 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-line ">description: [<a href="/reference/graphql-api/shop/object-types#localizedstring">LocalizedString</a>!]</div>
 
-<div class="graphql-code-line ">nullable: <a href="/reference/graphql-api/shop/object-types#boolean">Boolean</a></div>
-
 <div class="graphql-code-line ">ui: <a href="/reference/graphql-api/shop/object-types#json">JSON</a></div>
 
 
@@ -3190,7 +3195,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <div class="graphql-code-block">
 <div class="graphql-code-line top-level">union <span class="graphql-code-identifier">UpdateOrderItemsResult</span> =</div>
-<div class="graphql-code-line "><a href="/reference/graphql-api/shop/object-types#order">Order</a> | <a href="/reference/graphql-api/shop/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/shop/object-types#orderlimiterror">OrderLimitError</a> | <a href="/reference/graphql-api/shop/object-types#negativequantityerror">NegativeQuantityError</a> | <a href="/reference/graphql-api/shop/object-types#insufficientstockerror">InsufficientStockError</a></div>
+<div class="graphql-code-line "><a href="/reference/graphql-api/shop/object-types#order">Order</a> | <a href="/reference/graphql-api/shop/object-types#ordermodificationerror">OrderModificationError</a> | <a href="/reference/graphql-api/shop/object-types#orderlimiterror">OrderLimitError</a> | <a href="/reference/graphql-api/shop/object-types#negativequantityerror">NegativeQuantityError</a> | <a href="/reference/graphql-api/shop/object-types#insufficientstockerror">InsufficientStockError</a> | <a href="/reference/graphql-api/shop/object-types#orderinterceptorerror">OrderInterceptorError</a></div>
 </div>
 
 ## Upload

+ 1 - 1
docs/docs/reference/typescript-api/assets/asset-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## AssetOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="629" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="639" packageName="@vendure/core" />
 
 The AssetOptions define how assets (images and other files) are named and stored, and how preview images are generated.
 

+ 1 - 1
docs/docs/reference/typescript-api/auth/auth-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## AuthOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="331" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="332" packageName="@vendure/core" />
 
 The AuthOptions define how authentication and authorization is managed.
 

+ 1 - 1
docs/docs/reference/typescript-api/auth/cookie-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## CookieOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="226" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="227" packageName="@vendure/core" />
 
 Options for the handling of the cookies used to track sessions (only applicable if
 `authOptions.tokenMethod` is set to `'cookie'`). These options are passed directly

+ 1 - 1
docs/docs/reference/typescript-api/auth/superadmin-credentials.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## SuperadminCredentials
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="805" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="815" packageName="@vendure/core" />
 
 These credentials will be used to create the Superadmin user & administrator
 when Vendure first bootstraps.

+ 1 - 1
docs/docs/reference/typescript-api/common/currency-code.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## CurrencyCode
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="993" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="992" packageName="@vendure/common" />
 
 ISO 4217 currency code
 

+ 1 - 1
docs/docs/reference/typescript-api/common/entity-relation-paths.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## EntityRelationPaths
 
-<GenerationInfo sourceFile="packages/core/src/common/types/entity-relation-paths.ts" sourceLine="23" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/common/types/entity-relation-paths.ts" sourceLine="25" packageName="@vendure/core" />
 
 This type allows type-safe access to entity relations using strings with dot notation.
 It works to 2 levels deep.

+ 1 - 1
docs/docs/reference/typescript-api/common/job-state.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## JobState
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2231" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2228" packageName="@vendure/common" />
 
 The state of a Job in the JobQueue
 

+ 1 - 1
docs/docs/reference/typescript-api/common/language-code.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## LanguageCode
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2249" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="2246" packageName="@vendure/common" />
 
 Languages in the form of a ISO 639-1 language code with optional
 region or script modifier (e.g. de_AT). The selection available is based

+ 1 - 1
docs/docs/reference/typescript-api/common/permission.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## Permission
 
-<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="4392" packageName="@vendure/common" />
+<GenerationInfo sourceFile="packages/common/src/generated-types.ts" sourceLine="4411" packageName="@vendure/common" />
 
 Permissions for administrators and customers. Used to control access to
 GraphQL resolvers via the <a href='/reference/typescript-api/request/allow-decorator#allow'>Allow</a> decorator.

+ 9 - 9
docs/docs/reference/typescript-api/configurable-operation-def/default-form-config-hash.md

@@ -29,15 +29,15 @@ type DefaultFormConfigHash = {
     'product-selector-form-input': Record<string, never>;
     'relation-form-input': Record<string, never>;
     'rich-text-form-input': Record<string, never>;
-    'select-form-input': {
-        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
+    'select-form-input': {
+        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
     };
     'text-form-input': { prefix?: string; suffix?: string };
-    'textarea-form-input': {
-        spellcheck?: boolean;
+    'textarea-form-input': {
+        spellcheck?: boolean;
     };
-    'product-multi-form-input': {
-        selectionMode?: 'product' | 'variant';
+    'product-multi-form-input': {
+        selectionMode?: 'product' | 'variant';
     };
     'combination-mode-form-input': Record<string, never>;
     'struct-form-input': Record<string, never>;
@@ -108,7 +108,7 @@ type DefaultFormConfigHash = {
 
 ### 'select-form-input'
 
-<MemberInfo kind="property" type={`{         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;     }`}   />
+<MemberInfo kind="property" type={`{
         options?: Array&#60;{ value: string; label?: Array&#60;Omit&#60;LocalizedString, '__typename'&#62;&#62; }&#62;;
     }`}   />
 
 
 ### 'text-form-input'
@@ -118,12 +118,12 @@ type DefaultFormConfigHash = {
 
 ### 'textarea-form-input'
 
-<MemberInfo kind="property" type={`{         spellcheck?: boolean;     }`}   />
+<MemberInfo kind="property" type={`{
         spellcheck?: boolean;
     }`}   />
 
 
 ### 'product-multi-form-input'
 
-<MemberInfo kind="property" type={`{         selectionMode?: 'product' | 'variant';     }`}   />
+<MemberInfo kind="property" type={`{
         selectionMode?: 'product' | 'variant';
     }`}   />
 
 
 ### 'combination-mode-form-input'

+ 1 - 1
docs/docs/reference/typescript-api/configuration/api-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ApiOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="69" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="70" packageName="@vendure/core" />
 
 The ApiOptions define how the Vendure GraphQL APIs are exposed, as well as allowing the API layer
 to be extended with middleware.

+ 13 - 13
docs/docs/reference/typescript-api/configuration/entity-duplicator.md

@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/config/entity/entity-duplicator.ts" sourceLine="158" packageName="@vendure/core" since="2.2.0" />
 
-An EntityDuplicator is used to define the logic for duplicating entities when the `duplicateEntity` mutation is called.
+An EntityDuplicator is used to define the logic for duplicating entities when the `duplicateEntity` mutation is called.
 This allows you to add support for duplication of both core and custom entities.
 
 *Example*
@@ -102,11 +102,11 @@ export const config: VendureConfig = {
 ```ts title="Signature"
 class EntityDuplicator<T extends ConfigArgs = ConfigArgs> extends ConfigurableOperationDef<T> {
     constructor(config: EntityDuplicatorConfig<T>)
-    duplicate(input: {
-        ctx: RequestContext;
-        entityName: string;
-        id: ID;
-        args: ConfigArg[];
+    duplicate(input: {
+        ctx: RequestContext;
+        entityName: string;
+        id: ID;
+        args: ConfigArg[];
     }) => Promise<VendureEntity>;
 }
 ```
@@ -123,7 +123,7 @@ class EntityDuplicator<T extends ConfigArgs = ConfigArgs> extends ConfigurableOp
 
 ### duplicate
 
-<MemberInfo kind="method" type={`(input: {         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>;         entityName: string;         id: <a href='/reference/typescript-api/common/id#id'>ID</a>;         args: ConfigArg[];     }) => Promise&#60;<a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a>&#62;`}   />
+<MemberInfo kind="method" type={`(input: {
         ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>;
         entityName: string;
         id: <a href='/reference/typescript-api/common/id#id'>ID</a>;
         args: ConfigArg[];
     }) => Promise&#60;<a href='/reference/typescript-api/entities/vendure-entity#vendureentity'>VendureEntity</a>&#62;`}   />
 
 
 
@@ -138,11 +138,11 @@ class EntityDuplicator<T extends ConfigArgs = ConfigArgs> extends ConfigurableOp
 A function which performs the duplication of an entity.
 
 ```ts title="Signature"
-type DuplicateEntityFn<T extends ConfigArgs> = (input: {
-    ctx: RequestContext;
-    entityName: string;
-    id: ID;
-    args: ConfigArgValues<T>;
+type DuplicateEntityFn<T extends ConfigArgs> = (input: {
+    ctx: RequestContext;
+    entityName: string;
+    id: ID;
+    args: ConfigArgValues<T>;
 }) => Promise<VendureEntity>
 ```
 
@@ -170,7 +170,7 @@ interface EntityDuplicatorConfig<T extends ConfigArgs> extends ConfigurableOpera
 
 <MemberInfo kind="property" type={`Array&#60;<a href='/reference/typescript-api/common/permission#permission'>Permission</a> | string&#62; | <a href='/reference/typescript-api/common/permission#permission'>Permission</a> | string`}   />
 
-The permissions required in order to execute this duplicator. If an array is passed,
+The permissions required in order to execute this duplicator. If an array is passed,
 then the administrator must have at least one of the permissions in the array.
 ### forEntities
 

+ 1 - 1
docs/docs/reference/typescript-api/configuration/entity-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## EntityOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="955" packageName="@vendure/core" since="1.3.0" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="965" packageName="@vendure/core" since="1.3.0" />
 
 Options relating to the internal handling of entities.
 

+ 1 - 1
docs/docs/reference/typescript-api/configuration/runtime-vendure-config.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## RuntimeVendureConfig
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1211" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1221" packageName="@vendure/core" />
 
 This interface represents the VendureConfig object available at run-time, i.e. the user-supplied
 config values have been merged with the <a href='/reference/typescript-api/configuration/default-config#defaultconfig'>defaultConfig</a> values.

+ 1 - 1
docs/docs/reference/typescript-api/configuration/system-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## SystemOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1044" packageName="@vendure/core" since="1.6.0" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1054" packageName="@vendure/core" since="1.6.0" />
 
 Options relating to system functions.
 

+ 1 - 1
docs/docs/reference/typescript-api/configuration/vendure-config.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## VendureConfig
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1081" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="1091" packageName="@vendure/core" />
 
 All possible configuration options are defined by the
 [`VendureConfig`](https://github.com/vendure-ecommerce/vendure/blob/master/server/src/config/vendure-config.ts) interface.

+ 2 - 1
docs/docs/reference/typescript-api/custom-fields/index.md

@@ -13,7 +13,8 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/config/custom-field/custom-field-types.ts" sourceLine="264" packageName="@vendure/core" />
 
-Most entities can have additional fields added to them by defining an array of <a href='/reference/typescript-api/custom-fields/custom-field-config#customfieldconfig'>CustomFieldConfig</a>objects on against the corresponding key.
+Most entities can have additional fields added to them by defining an array of <a href='/reference/typescript-api/custom-fields/custom-field-config#customfieldconfig'>CustomFieldConfig</a>
+objects on against the corresponding key.
 
 *Example*
 

+ 2 - 1
docs/docs/reference/typescript-api/data-access/calculated.md

@@ -14,7 +14,8 @@ import MemberDescription from '@site/src/components/MemberDescription';
 <GenerationInfo sourceFile="packages/core/src/common/calculated-decorator.ts" sourceLine="43" packageName="@vendure/core" />
 
 Used to define calculated entity getters. The decorator simply attaches an array of "calculated"
-property names to the entity's prototype. This array is then used by the <a href='/reference/typescript-api/data-access/calculated-property-subscriber#calculatedpropertysubscriber'>CalculatedPropertySubscriber</a>to transfer the getter function from the prototype to the entity instance.
+property names to the entity's prototype. This array is then used by the <a href='/reference/typescript-api/data-access/calculated-property-subscriber#calculatedpropertysubscriber'>CalculatedPropertySubscriber</a>
+to transfer the getter function from the prototype to the entity instance.
 
 ```ts title="Signature"
 function Calculated(queryInstruction?: CalculatedColumnQueryInstruction): MethodDecorator

+ 48 - 48
docs/docs/reference/typescript-api/data-access/transactional-connection.md

@@ -13,12 +13,12 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/connection/transactional-connection.ts" sourceLine="41" packageName="@vendure/core" />
 
-The TransactionalConnection is a wrapper around the TypeORM `Connection` object which works in conjunction
-with the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator to implement per-request transactions. All services which access the
-database should use this class rather than the raw TypeORM connection, to ensure that db changes can be
-easily wrapped in transactions when required.
-
-The service layer does not need to know about the scope of a transaction, as this is covered at the
+The TransactionalConnection is a wrapper around the TypeORM `Connection` object which works in conjunction
+with the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator to implement per-request transactions. All services which access the
+database should use this class rather than the raw TypeORM connection, to ensure that db changes can be
+easily wrapped in transactions when required.
+
+The service layer does not need to know about the scope of a transaction, as this is covered at the
 API by the use of the `Transaction` decorator.
 
 ```ts title="Signature"
@@ -26,11 +26,11 @@ class TransactionalConnection {
     constructor(dataSource: DataSource, transactionWrapper: TransactionWrapper)
     rawConnection: DataSource
     getRepository(target: ObjectType<Entity> | EntitySchema<Entity> | string) => Repository<Entity>;
-    getRepository(ctx: RequestContext | undefined, target: ObjectType<Entity> | EntitySchema<Entity> | string, options?: {
-            replicationMode?: ReplicationMode;
+    getRepository(ctx: RequestContext | undefined, target: ObjectType<Entity> | EntitySchema<Entity> | string, options?: {
+            replicationMode?: ReplicationMode;
         }) => Repository<Entity>;
-    getRepository(ctxOrTarget: RequestContext | ObjectType<Entity> | EntitySchema<Entity> | string | undefined, maybeTarget?: ObjectType<Entity> | EntitySchema<Entity> | string, options?: {
-            replicationMode?: ReplicationMode;
+    getRepository(ctxOrTarget: RequestContext | ObjectType<Entity> | EntitySchema<Entity> | string | undefined, maybeTarget?: ObjectType<Entity> | EntitySchema<Entity> | string, options?: {
+            replicationMode?: ReplicationMode;
         }) => Repository<Entity>;
     withTransaction(work: (ctx: RequestContext) => Promise<T>) => Promise<T>;
     withTransaction(ctx: RequestContext, work: (ctx: RequestContext) => Promise<T>) => Promise<T>;
@@ -55,53 +55,53 @@ class TransactionalConnection {
 
 <MemberInfo kind="property" type={`DataSource`}   />
 
-The plain TypeORM Connection object. Should be used carefully as any operations
-performed with this connection will not be performed within any outer
+The plain TypeORM Connection object. Should be used carefully as any operations
+performed with this connection will not be performed within any outer
 transactions.
 ### getRepository
 
 <MemberInfo kind="method" type={`(target: ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string) => Repository&#60;Entity&#62;`}   />
 
-Returns a TypeORM repository. Note that when no RequestContext is supplied, the repository will not
-be aware of any existing transaction. Therefore, calling this method without supplying a RequestContext
+Returns a TypeORM repository. Note that when no RequestContext is supplied, the repository will not
+be aware of any existing transaction. Therefore, calling this method without supplying a RequestContext
 is discouraged without a deliberate reason.
 ### getRepository
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> | undefined, target: ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string, options?: {             replicationMode?: ReplicationMode;         }) => Repository&#60;Entity&#62;`}   />
-
-Returns a TypeORM repository which is bound to any existing transactions. It is recommended to _always_ pass
-the RequestContext argument when possible, otherwise the queries will be executed outside of any
-ongoing transactions which have been started by the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator.
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> | undefined, target: ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string, options?: {
             replicationMode?: ReplicationMode;
         }) => Repository&#60;Entity&#62;`}   />
 
-The `options` parameter allows specifying additional configurations, such as the `replicationMode`,
+Returns a TypeORM repository which is bound to any existing transactions. It is recommended to _always_ pass
+the RequestContext argument when possible, otherwise the queries will be executed outside of any
+ongoing transactions which have been started by the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator.
+
+The `options` parameter allows specifying additional configurations, such as the `replicationMode`,
 which determines whether the repository should interact with the master or replica database.
 ### getRepository
 
-<MemberInfo kind="method" type={`(ctxOrTarget: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> | ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string | undefined, maybeTarget?: ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string, options?: {             replicationMode?: ReplicationMode;         }) => Repository&#60;Entity&#62;`}   />
+<MemberInfo kind="method" type={`(ctxOrTarget: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> | ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string | undefined, maybeTarget?: ObjectType&#60;Entity&#62; | EntitySchema&#60;Entity&#62; | string, options?: {
             replicationMode?: ReplicationMode;
         }) => Repository&#60;Entity&#62;`}   />
 
-Returns a TypeORM repository. Depending on the parameters passed, it will either be transaction-aware
-or not. If `RequestContext` is provided, the repository is bound to any ongoing transactions. The
+Returns a TypeORM repository. Depending on the parameters passed, it will either be transaction-aware
+or not. If `RequestContext` is provided, the repository is bound to any ongoing transactions. The
 `options` parameter allows further customization, such as selecting the replication mode (e.g., 'master').
 ### withTransaction
 
 <MemberInfo kind="method" type={`(work: (ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) =&#62; Promise&#60;T&#62;) => Promise&#60;T&#62;`}  since="1.3.0"  />
 
-Allows database operations to be wrapped in a transaction, ensuring that in the event of an error being
-thrown at any point, the entire transaction will be rolled back and no changes will be saved.
-
-In the context of API requests, you should instead use the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator on your resolver or
-controller method.
-
-On the other hand, for code that does not run in the context of a GraphQL/REST request, this method
-should be used to protect against non-atomic changes to the data which could leave your data in an
-inconsistent state.
-
-Such situations include function processed by the JobQueue or stand-alone scripts which make use
-of Vendure internal services.
-
-If there is already a <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> object available, you should pass it in as the first
-argument in order to create transactional context as the copy. If not, omit the first argument and an empty
-RequestContext object will be created, which is then used to propagate the transaction to
+Allows database operations to be wrapped in a transaction, ensuring that in the event of an error being
+thrown at any point, the entire transaction will be rolled back and no changes will be saved.
+
+In the context of API requests, you should instead use the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator on your resolver or
+controller method.
+
+On the other hand, for code that does not run in the context of a GraphQL/REST request, this method
+should be used to protect against non-atomic changes to the data which could leave your data in an
+inconsistent state.
+
+Such situations include function processed by the JobQueue or stand-alone scripts which make use
+of Vendure internal services.
+
+If there is already a <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a> object available, you should pass it in as the first
+argument in order to create transactional context as the copy. If not, omit the first argument and an empty
+RequestContext object will be created, which is then used to propagate the transaction to
 all inner method calls.
 
 *Example*
@@ -137,40 +137,40 @@ private async transferCredit(outerCtx: RequestContext, fromId: ID, toId: ID, amo
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, isolationLevel?: <a href='/reference/typescript-api/request/transaction-decorator#transactionisolationlevel'>TransactionIsolationLevel</a>) => `}   />
 
-Manually start a transaction if one is not already in progress. This method should be used in
+Manually start a transaction if one is not already in progress. This method should be used in
 conjunction with the `'manual'` mode of the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator.
 ### commitOpenTransaction
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) => `}   />
 
-Manually commits any open transaction. Should be very rarely needed, since the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator
-and the internal TransactionInterceptor take care of this automatically. Use-cases include situations
-in which the worker thread needs to access changes made in the current transaction, or when using the
+Manually commits any open transaction. Should be very rarely needed, since the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator
+and the internal TransactionInterceptor take care of this automatically. Use-cases include situations
+in which the worker thread needs to access changes made in the current transaction, or when using the
 Transaction decorator in manual mode.
 ### rollBackTransaction
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>) => `}   />
 
-Manually rolls back any open transaction. Should be very rarely needed, since the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator
-and the internal TransactionInterceptor take care of this automatically. Use-cases include when using the
+Manually rolls back any open transaction. Should be very rarely needed, since the <a href='/reference/typescript-api/request/transaction-decorator#transaction'>Transaction</a> decorator
+and the internal TransactionInterceptor take care of this automatically. Use-cases include when using the
 Transaction decorator in manual mode.
 ### getEntityOrThrow
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, entityType: Type&#60;T&#62;, id: <a href='/reference/typescript-api/common/id#id'>ID</a>, options: <a href='/reference/typescript-api/data-access/get-entity-or-throw-options#getentityorthrowoptions'>GetEntityOrThrowOptions</a>&#60;T&#62; = {}) => Promise&#60;T&#62;`}   />
 
-Finds an entity of the given type by ID, or throws an `EntityNotFoundError` if none
+Finds an entity of the given type by ID, or throws an `EntityNotFoundError` if none
 is found.
 ### findOneInChannel
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, entity: Type&#60;T&#62;, id: <a href='/reference/typescript-api/common/id#id'>ID</a>, channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>, options: FindOneOptions&#60;T&#62; = {}) => `}   />
 
-Like the TypeOrm `Repository.findOne()` method, but limits the results to
+Like the TypeOrm `Repository.findOne()` method, but limits the results to
 the given Channel.
 ### findByIdsInChannel
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, entity: Type&#60;T&#62;, ids: <a href='/reference/typescript-api/common/id#id'>ID</a>[], channelId: <a href='/reference/typescript-api/common/id#id'>ID</a>, options: FindManyOptions&#60;T&#62;) => `}   />
 
-Like the TypeOrm `Repository.findByIds()` method, but limits the results to
+Like the TypeOrm `Repository.findByIds()` method, but limits the results to
 the given Channel.
 
 

+ 1 - 1
docs/docs/reference/typescript-api/default-search-plugin/index.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## DefaultSearchPlugin
 
-<GenerationInfo sourceFile="packages/core/src/plugin/default-search-plugin/default-search-plugin.ts" sourceLine="69" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/plugin/default-search-plugin/default-search-plugin.ts" sourceLine="70" packageName="@vendure/core" />
 
 The DefaultSearchPlugin provides a full-text Product search based on the full-text searching capabilities of the
 underlying database.

+ 30 - 30
docs/docs/reference/typescript-api/entities/promotion.md

@@ -13,42 +13,42 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/entity/promotion/promotion.entity.ts" sourceLine="61" packageName="@vendure/core" />
 
-A Promotion is used to define a set of conditions under which promotions actions (typically discounts)
-will be applied to an Order.
-
-Each assigned <a href='/reference/typescript-api/promotions/promotion-condition#promotioncondition'>PromotionCondition</a> is checked against the Order, and if they all return `true`,
+A Promotion is used to define a set of conditions under which promotions actions (typically discounts)
+will be applied to an Order.
+
+Each assigned <a href='/reference/typescript-api/promotions/promotion-condition#promotioncondition'>PromotionCondition</a> is checked against the Order, and if they all return `true`,
 then each assign <a href='/reference/typescript-api/promotions/promotion-action#promotionitemaction'>PromotionItemAction</a> / <a href='/reference/typescript-api/promotions/promotion-action#promotionlineaction'>PromotionLineAction</a> / <a href='/reference/typescript-api/promotions/promotion-action#promotionorderaction'>PromotionOrderAction</a> / <a href='/reference/typescript-api/promotions/promotion-action#promotionshippingaction'>PromotionShippingAction</a> is applied to the Order.
 
 ```ts title="Signature"
 class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable, HasCustomFields, Translatable {
     type = AdjustmentType.PROMOTION;
-    constructor(input?: DeepPartial<Promotion> & {
-            promotionConditions?: Array<PromotionCondition<any>>;
-            promotionActions?: Array<PromotionAction<any>>;
+    constructor(input?: DeepPartial<Promotion> & {
+            promotionConditions?: Array<PromotionCondition<any>>;
+            promotionActions?: Array<PromotionAction<any>>;
         })
-    @Column({ type: Date, nullable: true })
+    @Column({ type: Date, nullable: true })
     deletedAt: Date | null;
-    @Column({ type: Date, nullable: true })
+    @Column({ type: Date, nullable: true })
     startsAt: Date | null;
-    @Column({ type: Date, nullable: true })
+    @Column({ type: Date, nullable: true })
     endsAt: Date | null;
-    @Column({ nullable: true })
+    @Column({ nullable: true })
     couponCode: string;
-    @Column({ nullable: true })
+    @Column({ nullable: true })
     perCustomerUsageLimit: number;
-    @Column({ nullable: true })
+    @Column({ nullable: true })
     usageLimit: number;
     name: LocaleString;
     description: LocaleString;
-    @OneToMany(type => PromotionTranslation, translation => translation.base, { eager: true })
+    @OneToMany(type => PromotionTranslation, translation => translation.base, { eager: true })
     translations: Array<Translation<Promotion>>;
     @Column() enabled: boolean;
-    @ManyToMany(type => Channel, channel => channel.promotions)
-    @JoinTable()
+    @ManyToMany(type => Channel, channel => channel.promotions)
+    @JoinTable()
     channels: Channel[];
-    @ManyToMany(type => Order, order => order.promotions)
+    @ManyToMany(type => Order, order => order.promotions)
     orders: Order[];
-    @Column(type => CustomPromotionFields)
+    @Column(type => CustomPromotionFields)
     customFields: CustomPromotionFields;
     @Column('simple-json') conditions: ConfigurableOperation[];
     @Column('simple-json') actions: ConfigurableOperation[];
@@ -75,7 +75,7 @@ class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable,
 
 ### constructor
 
-<MemberInfo kind="method" type={`(input?: DeepPartial&#60;<a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a>&#62; &#38; {             promotionConditions?: Array&#60;<a href='/reference/typescript-api/promotions/promotion-condition#promotioncondition'>PromotionCondition</a>&#60;any&#62;&#62;;             promotionActions?: Array&#60;<a href='/reference/typescript-api/promotions/promotion-action#promotionaction'>PromotionAction</a>&#60;any&#62;&#62;;         }) => Promotion`}   />
+<MemberInfo kind="method" type={`(input?: DeepPartial&#60;<a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a>&#62; &#38; {
             promotionConditions?: Array&#60;<a href='/reference/typescript-api/promotions/promotion-condition#promotioncondition'>PromotionCondition</a>&#60;any&#62;&#62;;
             promotionActions?: Array&#60;<a href='/reference/typescript-api/promotions/promotion-action#promotionaction'>PromotionAction</a>&#60;any&#62;&#62;;
         }) => Promotion`}   />
 
 
 ### deletedAt
@@ -157,17 +157,17 @@ class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable,
 
 <MemberInfo kind="property" type={`number`}   />
 
-The PriorityScore is used to determine the sequence in which multiple promotions are tested
-on a given order. A higher number moves the Promotion towards the end of the sequence.
-
-The score is derived from the sum of the priorityValues of the PromotionConditions and
-PromotionActions comprising this Promotion.
-
-An example illustrating the need for a priority is this:
-
-
-Consider 2 Promotions, 1) buy 1 get one free and 2) 10% off when order total is over $50.
-If Promotion 2 is evaluated prior to Promotion 1, then it can trigger the 10% discount even
+The PriorityScore is used to determine the sequence in which multiple promotions are tested
+on a given order. A higher number moves the Promotion towards the end of the sequence.
+
+The score is derived from the sum of the priorityValues of the PromotionConditions and
+PromotionActions comprising this Promotion.
+
+An example illustrating the need for a priority is this:
+
+
+Consider 2 Promotions, 1) buy 1 get one free and 2) 10% off when order total is over $50.
+If Promotion 2 is evaluated prior to Promotion 1, then it can trigger the 10% discount even
 if the subsequent application of Promotion 1 brings the order total down to way below $50.
 ### apply
 

+ 44 - 44
docs/docs/reference/typescript-api/events/event-types.md

@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/account-registration-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired when a new user registers an account, either as a stand-alone signup or after
+This event is fired when a new user registers an account, either as a stand-alone signup or after
 placing an order.
 
 ```ts title="Signature"
@@ -41,7 +41,7 @@ class AccountRegistrationEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/account-verified-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired when a users email address successfully gets verified after
+This event is fired when a users email address successfully gets verified after
 the `verifyCustomerAccount` mutation was executed.
 
 ```ts title="Signature"
@@ -96,7 +96,7 @@ class AdministratorEvent extends VendureEntityEvent<Administrator, Administrator
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/asset-channel-event.ts" sourceLine="15" packageName="@vendure/core" />
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/asset#asset'>Asset</a> is assigned or removed
+This event is fired whenever an <a href='/reference/typescript-api/entities/asset#asset'>Asset</a> is assigned or removed
 From a channel.
 
 ```ts title="Signature"
@@ -157,8 +157,8 @@ class AssetEvent extends VendureEntityEvent<Asset, AssetInputTypes> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/attempted-login-event.ts" sourceLine="14" packageName="@vendure/core" />
 
-This event is fired when an attempt is made to log in via the shop or admin API `login` mutation.
-The `strategy` represents the name of the AuthenticationStrategy used in the login attempt.
+This event is fired when an attempt is made to log in via the shop or admin API `login` mutation.
+The `strategy` represents the name of the AuthenticationStrategy used in the login attempt.
 If the "native" strategy is used, the additional `identifier` property will be available.
 
 ```ts title="Signature"
@@ -186,7 +186,7 @@ class AttemptedLoginEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/change-channel-event.ts" sourceLine="17" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/interfaces#channelaware'>ChannelAware</a> entity is assigned or removed
+This event is fired whenever an <a href='/reference/typescript-api/entities/interfaces#channelaware'>ChannelAware</a> entity is assigned or removed
 from a channel. The entity property contains the value before updating the channels.
 
 ```ts title="Signature"
@@ -268,10 +268,10 @@ class CollectionEvent extends VendureEntityEvent<Collection, CollectionInputType
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/collection-modification-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a Collection is modified in some way. The `productVariantIds`
-argument is an array of ids of all ProductVariants which:
-
-1. were part of this collection prior to modification and are no longer
+This event is fired whenever a Collection is modified in some way. The `productVariantIds`
+argument is an array of ids of all ProductVariants which:
+
+1. were part of this collection prior to modification and are no longer
 2. are now part of this collection after modification but were not before
 
 ```ts title="Signature"
@@ -326,7 +326,7 @@ class CountryEvent extends VendureEntityEvent<Country, CountryInputTypes> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/coupon-code-event.ts" sourceLine="15" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever an coupon code of an active <a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a>
+This event is fired whenever an coupon code of an active <a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a>
 is assigned or removed to an <a href='/reference/typescript-api/entities/order#order'>Order</a>.
 
 ```ts title="Signature"
@@ -354,7 +354,7 @@ class CouponCodeEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/customer-address-event.ts" sourceLine="22" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/address#address'>Address</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/address#address'>Address</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -388,7 +388,7 @@ class CustomerAddressEvent extends VendureEntityEvent<Address, CustomerAddressIn
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/customer-event.ts" sourceLine="22" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/customer#customer'>Customer</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/customer#customer'>Customer</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -422,7 +422,7 @@ class CustomerEvent extends VendureEntityEvent<Customer, CustomerInputTypes> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/customer-group-change-event.ts" sourceLine="15" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever one or more <a href='/reference/typescript-api/entities/customer#customer'>Customer</a> is assigned to or removed from a
+This event is fired whenever one or more <a href='/reference/typescript-api/entities/customer#customer'>Customer</a> is assigned to or removed from a
 <a href='/reference/typescript-api/entities/customer-group#customergroup'>CustomerGroup</a>.
 
 ```ts title="Signature"
@@ -585,7 +585,7 @@ class FulfillmentStateTransitionEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/global-settings-event.ts" sourceLine="16" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/global-settings#globalsettings'>GlobalSettings</a> is added. The type is always `updated`, because it's
+This event is fired whenever a <a href='/reference/typescript-api/entities/global-settings#globalsettings'>GlobalSettings</a> is added. The type is always `updated`, because it's
 only created once and never deleted.
 
 ```ts title="Signature"
@@ -646,7 +646,7 @@ class HistoryEntryEvent extends VendureEntityEvent<HistoryEntry, HistoryInput> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/identifier-change-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired when a registered user successfully changes the identifier (ie email address)
+This event is fired when a registered user successfully changes the identifier (ie email address)
 associated with their account.
 
 ```ts title="Signature"
@@ -674,7 +674,7 @@ class IdentifierChangeEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/identifier-change-request-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired when a registered user requests to update the identifier (ie email address)
+This event is fired when a registered user requests to update the identifier (ie email address)
 associated with the account.
 
 ```ts title="Signature"
@@ -783,7 +783,7 @@ class LogoutEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/order-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/order#order'>Order</a> is added, updated
+This event is fired whenever an <a href='/reference/typescript-api/entities/order#order'>Order</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -811,7 +811,7 @@ class OrderEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/order-line-event.ts" sourceLine="13" packageName="@vendure/core" />
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a> is added, updated
+This event is fired whenever an <a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -839,10 +839,10 @@ class OrderLineEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/order-placed-event.ts" sourceLine="17" packageName="@vendure/core" />
 
-This event is fired whenever an <a href='/reference/typescript-api/entities/order#order'>Order</a> is set as "placed", which by default is
-when it transitions from 'ArrangingPayment' to either 'PaymentAuthorized' or 'PaymentSettled'.
-
-Note that the exact point that it is set as "placed" can be configured according to the
+This event is fired whenever an <a href='/reference/typescript-api/entities/order#order'>Order</a> is set as "placed", which by default is
+when it transitions from 'ArrangingPayment' to either 'PaymentAuthorized' or 'PaymentSettled'.
+
+Note that the exact point that it is set as "placed" can be configured according to the
 <a href='/reference/typescript-api/orders/order-placed-strategy#orderplacedstrategy'>OrderPlacedStrategy</a>.
 
 ```ts title="Signature"
@@ -951,7 +951,7 @@ class PasswordResetVerifiedEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/payment-method-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -979,7 +979,7 @@ class PaymentMethodEvent extends VendureEntityEvent<PaymentMethod, PaymentMethod
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/payment-state-transition-event.ts" sourceLine="15" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/payment#payment'>Payment</a> transitions from one <a href='/reference/typescript-api/payment/payment-state#paymentstate'>PaymentState</a> to another, e.g.
+This event is fired whenever a <a href='/reference/typescript-api/entities/payment#payment'>Payment</a> transitions from one <a href='/reference/typescript-api/payment/payment-state#paymentstate'>PaymentState</a> to another, e.g.
 a Payment is authorized by the payment provider.
 
 ```ts title="Signature"
@@ -1007,7 +1007,7 @@ class PaymentStateTransitionEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/product-channel-event.ts" sourceLine="15" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/product#product'>Product</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/product#product'>Product</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1035,7 +1035,7 @@ class ProductChannelEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/product-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/product#product'>Product</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/product#product'>Product</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1126,14 +1126,14 @@ class ProductOptionGroupChangeEvent extends VendureEvent {
 This event is fired whenever a <a href='/reference/typescript-api/entities/product-option-group#productoptiongroup'>ProductOptionGroup</a> is added or updated.
 
 ```ts title="Signature"
-class ProductOptionGroupEvent extends VendureEntityEvent<
-    ProductOptionGroup,
-    ProductOptionGroupInputTypes
+class ProductOptionGroupEvent extends VendureEntityEvent<
+    ProductOptionGroup,
+    ProductOptionGroupInputTypes
 > {
     constructor(ctx: RequestContext, entity: ProductOptionGroup, type: 'created' | 'updated' | 'deleted', input?: ProductOptionGroupInputTypes)
 }
 ```
-* Extends: <code><a href='/reference/typescript-api/events/vendure-entity-event#vendureentityevent'>VendureEntityEvent</a>&#60;     <a href='/reference/typescript-api/entities/product-option-group#productoptiongroup'>ProductOptionGroup</a>,     ProductOptionGroupInputTypes &#62;</code>
+* Extends: <code><a href='/reference/typescript-api/events/vendure-entity-event#vendureentityevent'>VendureEntityEvent</a>&#60;
     <a href='/reference/typescript-api/entities/product-option-group#productoptiongroup'>ProductOptionGroup</a>,
     ProductOptionGroupInputTypes
 &#62;</code>
 
 
 
@@ -1180,7 +1180,7 @@ class ProductVariantChannelEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/product-variant-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/product-variant#productvariant'>ProductVariant</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1217,14 +1217,14 @@ class ProductVariantEvent extends VendureEntityEvent<ProductVariant[], ProductVa
 This event is fired whenever a <a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a> is added, updated or deleted.
 
 ```ts title="Signature"
-class ProductVariantPriceEvent extends VendureEntityEvent<
-    ProductVariantPrice[],
-    ProductVariantInputTypes
+class ProductVariantPriceEvent extends VendureEntityEvent<
+    ProductVariantPrice[],
+    ProductVariantInputTypes
 > {
     constructor(ctx: RequestContext, entity: ProductVariantPrice[], type: 'created' | 'updated' | 'deleted', input?: ProductVariantInputTypes)
 }
 ```
-* Extends: <code><a href='/reference/typescript-api/events/vendure-entity-event#vendureentityevent'>VendureEntityEvent</a>&#60;     <a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a>[],     ProductVariantInputTypes &#62;</code>
+* Extends: <code><a href='/reference/typescript-api/events/vendure-entity-event#vendureentityevent'>VendureEntityEvent</a>&#60;
     <a href='/reference/typescript-api/entities/product-variant-price#productvariantprice'>ProductVariantPrice</a>[],
     ProductVariantInputTypes
 &#62;</code>
 
 
 
@@ -1244,7 +1244,7 @@ class ProductVariantPriceEvent extends VendureEntityEvent<
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/promotion-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/promotion#promotion'>Promotion</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1353,7 +1353,7 @@ class RefundStateTransitionEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/role-change-event.ts" sourceLine="16" packageName="@vendure/core" since="1.4" />
 
-This event is fired whenever one <a href='/reference/typescript-api/entities/role#role'>Role</a> is assigned or removed from a user.
+This event is fired whenever one <a href='/reference/typescript-api/entities/role#role'>Role</a> is assigned or removed from a user.
 The property `roleIds` only contains the removed or assigned role ids.
 
 ```ts title="Signature"
@@ -1462,7 +1462,7 @@ class SellerEvent extends VendureEntityEvent<Seller, SellerInputTypes> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/shipping-method-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/shipping-method#shippingmethod'>ShippingMethod</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/shipping-method#shippingmethod'>ShippingMethod</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1490,7 +1490,7 @@ class ShippingMethodEvent extends VendureEntityEvent<ShippingMethod, ShippingMet
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/stock-movement-event.ts" sourceLine="16" packageName="@vendure/core" since="1.1.0" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/stock-movement#stockmovement'>StockMovement</a> entity is created, which occurs when the saleable
+This event is fired whenever a <a href='/reference/typescript-api/entities/stock-movement#stockmovement'>StockMovement</a> entity is created, which occurs when the saleable
 stock level of a ProductVariant is altered due to things like sales, manual adjustments, and cancellations.
 
 ```ts title="Signature"
@@ -1524,7 +1524,7 @@ class StockMovementEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/tax-category-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/tax-category#taxcategory'>TaxCategory</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/tax-category#taxcategory'>TaxCategory</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1552,7 +1552,7 @@ class TaxCategoryEvent extends VendureEntityEvent<TaxCategory, TaxCategoryInputT
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/tax-rate-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/tax-rate#taxrate'>TaxRate</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1607,7 +1607,7 @@ class TaxRateModificationEvent extends VendureEvent {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/zone-event.ts" sourceLine="18" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/zone#zone'>Zone</a> is added, updated
+This event is fired whenever a <a href='/reference/typescript-api/entities/zone#zone'>Zone</a> is added, updated
 or deleted.
 
 ```ts title="Signature"
@@ -1635,7 +1635,7 @@ class ZoneEvent extends VendureEntityEvent<Zone, ZoneInputTypes> {
 
 <GenerationInfo sourceFile="packages/core/src/event-bus/events/zone-members-event.ts" sourceLine="15" packageName="@vendure/core" />
 
-This event is fired whenever a <a href='/reference/typescript-api/entities/zone#zone'>Zone</a> gets <a href='/reference/typescript-api/entities/country#country'>Country</a> members assigned or removed
+This event is fired whenever a <a href='/reference/typescript-api/entities/zone#zone'>Zone</a> gets <a href='/reference/typescript-api/entities/country#country'>Country</a> members assigned or removed
 The `entity` property contains the zone with the already updated member field.
 
 ```ts title="Signature"

+ 14 - 14
docs/docs/reference/typescript-api/fulfillment/fulfillment-process.md

@@ -13,13 +13,13 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/config/fulfillment/default-fulfillment-process.ts" sourceLine="45" packageName="@vendure/core" since="2.0.0" />
 
-The default <a href='/reference/typescript-api/fulfillment/fulfillment-process#fulfillmentprocess'>FulfillmentProcess</a>. This process includes the following actions:
-
-- Executes the configured `FulfillmentHandler.onFulfillmentTransition()` before any state
-  transition.
-- On cancellation of a Fulfillment, creates the necessary <a href='/reference/typescript-api/entities/stock-movement#cancellation'>Cancellation</a> & <a href='/reference/typescript-api/entities/stock-movement#allocation'>Allocation</a>
-  stock movement records.
-- When a Fulfillment transitions from the `Created` to `Pending` state, the necessary
+The default <a href='/reference/typescript-api/fulfillment/fulfillment-process#fulfillmentprocess'>FulfillmentProcess</a>. This process includes the following actions:
+
+- Executes the configured `FulfillmentHandler.onFulfillmentTransition()` before any state
+  transition.
+- On cancellation of a Fulfillment, creates the necessary <a href='/reference/typescript-api/entities/stock-movement#cancellation'>Cancellation</a> & <a href='/reference/typescript-api/entities/stock-movement#allocation'>Allocation</a>
+  stock movement records.
+- When a Fulfillment transitions from the `Created` to `Pending` state, the necessary
   <a href='/reference/typescript-api/entities/stock-movement#sale'>Sale</a> stock movements are created.
 
 
@@ -28,16 +28,16 @@ The default <a href='/reference/typescript-api/fulfillment/fulfillment-process#f
 
 <GenerationInfo sourceFile="packages/core/src/config/fulfillment/fulfillment-process.ts" sourceLine="26" packageName="@vendure/core" since="2.0.0" />
 
-A FulfillmentProcess is used to define the way the fulfillment process works as in: what states a Fulfillment can be
-in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, a
-FulfillmentProcess can perform checks before allowing a state transition to occur, and the `onTransitionEnd()`
-hook allows logic to be executed after a state change.
-
+A FulfillmentProcess is used to define the way the fulfillment process works as in: what states a Fulfillment can be
+in, and how it may transition from one state to another. Using the `onTransitionStart()` hook, a
+FulfillmentProcess can perform checks before allowing a state transition to occur, and the `onTransitionEnd()`
+hook allows logic to be executed after a state change.
+
 For detailed description of the interface members, see the <a href='/reference/typescript-api/state-machine/state-machine-config#statemachineconfig'>StateMachineConfig</a> docs.
 
 ```ts title="Signature"
 interface FulfillmentProcess<State extends keyof CustomFulfillmentStates | string> extends InjectableStrategy {
-    transitions?: Transitions<State, State | FulfillmentState> &
+    transitions?: Transitions<State, State | FulfillmentState> &
         Partial<Transitions<FulfillmentState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | FulfillmentState, FulfillmentTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | FulfillmentState, FulfillmentTransitionData>;
@@ -52,7 +52,7 @@ interface FulfillmentProcess<State extends keyof CustomFulfillmentStates | strin
 
 ### transitions
 
-<MemberInfo kind="property" type={`<a href='/reference/typescript-api/state-machine/transitions#transitions'>Transitions</a>&#60;State, State | <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>&#62; &#38;         Partial&#60;<a href='/reference/typescript-api/state-machine/transitions#transitions'>Transitions</a>&#60;<a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a> | State&#62;&#62;`}   />
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/state-machine/transitions#transitions'>Transitions</a>&#60;State, State | <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>&#62; &#38;
         Partial&#60;<a href='/reference/typescript-api/state-machine/transitions#transitions'>Transitions</a>&#60;<a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a> | State&#62;&#62;`}   />
 
 
 ### onTransitionStart

+ 5 - 5
docs/docs/reference/typescript-api/import-export/default-asset-import-strategy.md

@@ -13,14 +13,14 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/config/asset-import-strategy/default-asset-import-strategy.ts" sourceLine="50" packageName="@vendure/core" since="1.7.0" />
 
-The DefaultAssetImportStrategy is able to import paths from the local filesystem (taking into account the
+The DefaultAssetImportStrategy is able to import paths from the local filesystem (taking into account the
 `importExportOptions.importAssetsDir` setting) as well as remote http/https urls.
 
 ```ts title="Signature"
 class DefaultAssetImportStrategy implements AssetImportStrategy {
-    constructor(options?: {
-            retryDelayMs: number;
-            retryCount: number;
+    constructor(options?: {
+            retryDelayMs: number;
+            retryCount: number;
         })
     init(injector: Injector) => ;
     getStreamFromPath(assetPath: string) => ;
@@ -34,7 +34,7 @@ class DefaultAssetImportStrategy implements AssetImportStrategy {
 
 ### constructor
 
-<MemberInfo kind="method" type={`(options?: {             retryDelayMs: number;             retryCount: number;         }) => DefaultAssetImportStrategy`}   />
+<MemberInfo kind="method" type={`(options?: {
             retryDelayMs: number;
             retryCount: number;
         }) => DefaultAssetImportStrategy`}   />
 
 
 ### init

+ 1 - 1
docs/docs/reference/typescript-api/import-export/import-export-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ImportExportOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="890" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="900" packageName="@vendure/core" />
 
 Options related to importing & exporting data.
 

+ 33 - 33
docs/docs/reference/typescript-api/import-export/import-parser.md

@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="152" packageName="@vendure/core" />
 
-Validates and parses CSV files into a data structure which can then be used to created new entities.
+Validates and parses CSV files into a data structure which can then be used to created new entities.
 This is used internally by the <a href='/reference/typescript-api/import-export/importer#importer'>Importer</a>.
 
 ```ts title="Signature"
@@ -28,7 +28,7 @@ class ImportParser {
 
 <MemberInfo kind="method" type={`(input: string | Stream, mainLanguage: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a> = this.configService.defaultLanguageCode) => Promise&#60;<a href='/reference/typescript-api/import-export/import-parser#parseresult'>ParseResult</a>&#60;<a href='/reference/typescript-api/import-export/import-parser#parsedproductwithvariants'>ParsedProductWithVariants</a>&#62;&#62;`}   />
 
-Parses the contents of the [product import CSV file](/guides/developer-guide/importing-data/#product-import-format) and
+Parses the contents of the [product import CSV file](/guides/developer-guide/importing-data/#product-import-format) and
 returns a data structure which can then be used to populate Vendure using the <a href='/reference/typescript-api/import-export/fast-importer-service#fastimporterservice'>FastImporterService</a>.
 
 
@@ -39,15 +39,15 @@ returns a data structure which can then be used to populate Vendure using the <a
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="45" packageName="@vendure/core" />
 
-The intermediate representation of an OptionGroup after it has been parsed
+The intermediate representation of an OptionGroup after it has been parsed
 by the <a href='/reference/typescript-api/import-export/import-parser#importparser'>ImportParser</a>.
 
 ```ts title="Signature"
 interface ParsedOptionGroup {
-    translations: Array<{
-        languageCode: LanguageCode;
-        name: string;
-        values: string[];
+    translations: Array<{
+        languageCode: LanguageCode;
+        name: string;
+        values: string[];
     }>;
 }
 ```
@@ -56,7 +56,7 @@ interface ParsedOptionGroup {
 
 ### translations
 
-<MemberInfo kind="property" type={`Array&#60;{         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         name: string;         values: string[];     }&#62;`}   />
+<MemberInfo kind="property" type={`Array&#60;{
         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         name: string;
         values: string[];
     }&#62;`}   />
 
 
 
@@ -68,15 +68,15 @@ interface ParsedOptionGroup {
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="61" packageName="@vendure/core" />
 
-The intermediate representation of a Facet after it has been parsed
+The intermediate representation of a Facet after it has been parsed
 by the <a href='/reference/typescript-api/import-export/import-parser#importparser'>ImportParser</a>.
 
 ```ts title="Signature"
 interface ParsedFacet {
-    translations: Array<{
-        languageCode: LanguageCode;
-        facet: string;
-        value: string;
+    translations: Array<{
+        languageCode: LanguageCode;
+        facet: string;
+        value: string;
     }>;
 }
 ```
@@ -85,7 +85,7 @@ interface ParsedFacet {
 
 ### translations
 
-<MemberInfo kind="property" type={`Array&#60;{         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         facet: string;         value: string;     }&#62;`}   />
+<MemberInfo kind="property" type={`Array&#60;{
         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         facet: string;
         value: string;
     }&#62;`}   />
 
 
 
@@ -97,7 +97,7 @@ interface ParsedFacet {
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="77" packageName="@vendure/core" />
 
-The intermediate representation of a ProductVariant after it has been parsed
+The intermediate representation of a ProductVariant after it has been parsed
 by the <a href='/reference/typescript-api/import-export/import-parser#importparser'>ImportParser</a>.
 
 ```ts title="Signature"
@@ -109,12 +109,12 @@ interface ParsedProductVariant {
     trackInventory: GlobalFlag;
     assetPaths: string[];
     facets: ParsedFacet[];
-    translations: Array<{
-        languageCode: LanguageCode;
-        optionValues: string[];
-        customFields: {
-            [name: string]: string;
-        };
+    translations: Array<{
+        languageCode: LanguageCode;
+        optionValues: string[];
+        customFields: {
+            [name: string]: string;
+        };
     }>;
 }
 ```
@@ -158,7 +158,7 @@ interface ParsedProductVariant {
 
 ### translations
 
-<MemberInfo kind="property" type={`Array&#60;{         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         optionValues: string[];         customFields: {             [name: string]: string;         };     }&#62;`}   />
+<MemberInfo kind="property" type={`Array&#60;{
         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         optionValues: string[];
         customFields: {
             [name: string]: string;
         };
     }&#62;`}   />
 
 
 
@@ -170,7 +170,7 @@ interface ParsedProductVariant {
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="102" packageName="@vendure/core" />
 
-The intermediate representation of a Product after it has been parsed
+The intermediate representation of a Product after it has been parsed
 by the <a href='/reference/typescript-api/import-export/import-parser#importparser'>ImportParser</a>.
 
 ```ts title="Signature"
@@ -178,14 +178,14 @@ interface ParsedProduct {
     assetPaths: string[];
     optionGroups: ParsedOptionGroup[];
     facets: ParsedFacet[];
-    translations: Array<{
-        languageCode: LanguageCode;
-        name: string;
-        slug: string;
-        description: string;
-        customFields: {
-            [name: string]: string;
-        };
+    translations: Array<{
+        languageCode: LanguageCode;
+        name: string;
+        slug: string;
+        description: string;
+        customFields: {
+            [name: string]: string;
+        };
     }>;
 }
 ```
@@ -209,7 +209,7 @@ interface ParsedProduct {
 
 ### translations
 
-<MemberInfo kind="property" type={`Array&#60;{         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         name: string;         slug: string;         description: string;         customFields: {             [name: string]: string;         };     }&#62;`}   />
+<MemberInfo kind="property" type={`Array&#60;{
         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         name: string;
         slug: string;
         description: string;
         customFields: {
             [name: string]: string;
         };
     }&#62;`}   />
 
 
 
@@ -221,7 +221,7 @@ interface ParsedProduct {
 
 <GenerationInfo sourceFile="packages/core/src/data-import/providers/import-parser/import-parser.ts" sourceLine="125" packageName="@vendure/core" />
 
-The data structure into which an import CSV file is parsed by the
+The data structure into which an import CSV file is parsed by the
 <a href='/reference/typescript-api/import-export/import-parser#importparser'>ImportParser</a> `parseProducts()` method.
 
 ```ts title="Signature"

+ 1 - 1
docs/docs/reference/typescript-api/job-queue/job-queue-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## JobQueueOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="914" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="924" packageName="@vendure/core" />
 
 Options related to the built-in job queue.
 

+ 2 - 1
docs/docs/reference/typescript-api/job-queue/types.md

@@ -106,7 +106,8 @@ should resolve when the job is complete, or be rejected in case of an error.
 
 <GenerationInfo sourceFile="packages/core/src/job-queue/types.ts" sourceLine="37" packageName="@vendure/core" />
 
-A JSON-serializable data type which provides a <a href='/reference/typescript-api/job-queue/job#job'>Job</a>with the data it needs to be processed.
+A JSON-serializable data type which provides a <a href='/reference/typescript-api/job-queue/job#job'>Job</a>
+with the data it needs to be processed.
 
 ```ts title="Signature"
 type JobData<T> = JsonCompatible<T>

+ 25 - 25
docs/docs/reference/typescript-api/orders/guest-checkout-strategy.md

@@ -13,33 +13,33 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/config/order/guest-checkout-strategy.ts" sourceLine="33" packageName="@vendure/core" since="2.0.0" />
 
-A strategy that determines how to deal with guest checkouts - i.e. when a customer
-checks out without being logged in. For example, a strategy could be used to implement
-business rules such as:
-
-- No guest checkouts allowed
-- No guest checkouts allowed for customers who already have an account
-- No guest checkouts allowed for customers who have previously placed an order
-- Allow guest checkouts, but create a new Customer entity if the email address
-  is already in use
-- Allow guest checkouts, but update the existing Customer entity if the email address
-  is already in use
-
-:::info
-
-This is configured via the `orderOptions.guestCheckoutStrategy` property of
-your VendureConfig.
-
+A strategy that determines how to deal with guest checkouts - i.e. when a customer
+checks out without being logged in. For example, a strategy could be used to implement
+business rules such as:
+
+- No guest checkouts allowed
+- No guest checkouts allowed for customers who already have an account
+- No guest checkouts allowed for customers who have previously placed an order
+- Allow guest checkouts, but create a new Customer entity if the email address
+  is already in use
+- Allow guest checkouts, but update the existing Customer entity if the email address
+  is already in use
+
+:::info
+
+This is configured via the `orderOptions.guestCheckoutStrategy` property of
+your VendureConfig.
+
 :::
 
 ```ts title="Signature"
 interface GuestCheckoutStrategy extends InjectableStrategy {
-    setCustomerForOrder(
-        ctx: RequestContext,
-        order: Order,
-        input: CreateCustomerInput,
-    ):
-        | ErrorResultUnion<SetCustomerForOrderResult, Customer>
+    setCustomerForOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: CreateCustomerInput,
+    ):
+        | ErrorResultUnion<SetCustomerForOrderResult, Customer>
         | Promise<ErrorResultUnion<SetCustomerForOrderResult, Customer>>;
 }
 ```
@@ -51,9 +51,9 @@ interface GuestCheckoutStrategy extends InjectableStrategy {
 
 ### setCustomerForOrder
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>, input: CreateCustomerInput) => | <a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;SetCustomerForOrderResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;         | Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;SetCustomerForOrderResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>, input: CreateCustomerInput) => | <a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;SetCustomerForOrderResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;
         | Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;SetCustomerForOrderResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 
-This method is called when the `setCustomerForOrder` mutation is executed.
+This method is called when the `setCustomerForOrder` mutation is executed.
 It should return either a Customer object or an ErrorResult.
 
 

+ 254 - 0
docs/docs/reference/typescript-api/orders/order-interceptor.md

@@ -0,0 +1,254 @@
+---
+title: "OrderInterceptor"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## OrderInterceptor
+
+<GenerationInfo sourceFile="packages/core/src/config/order/order-interceptor.ts" sourceLine="214" packageName="@vendure/core" since="3.1.0" />
+
+An OrderInterceptor is a class which can be used to intercept and modify the behavior of order-related
+operations.
+
+It does this by providing methods which are called whenever the contents of an order are about
+to get changed. These methods are able to prevent the operation from proceeding by returning a string
+error message.
+
+Examples of use-cases for an OrderInterceptor include:
+
+* Preventing certain products from being added to the order based on some criteria, e.g. if the
+  product is already in another active order.
+* Enforcing a minimum or maximum quantity of a given product in the order
+* Using a CAPTCHA to prevent automated order creation
+
+:::info
+
+This is configured via the `orderOptions.orderInterceptors` property of
+your VendureConfig.
+
+:::
+
+OrderInterceptors are executed when the following mutations are called:
+
+- `addItemToOrder`
+- `adjustOrderLine`
+- `removeItemFromOrder`
+
+Additionally, if you are working directly with the <a href='/reference/typescript-api/services/order-service#orderservice'>OrderService</a>, the following methods will trigger
+any registered OrderInterceptors:
+
+- `addItemToOrder`
+- `addItemsToOrder`
+- `adjustOrderLine`
+- `adjustOrderLines`
+- `removeItemFromOrder`
+- `removeItemsFromOrder`
+
+When an OrderInterceptor is registered, it will be called in the order in which it was registered.
+If an interceptor method resolves to a string, the operation will be prevented and the string will be used as the error message.
+
+When multiple interceptors are registered, the first interceptor to resolve to a string will prevent the operation from proceeding.
+
+Errors returned by OrderInterceptors are surfaced to the GraphQL API as an `OrderInterceptorError` and can be
+queried like this:
+
+```graphql
+mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
+  addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
+    ... on Order {
+      id
+      code
+      # ... other Order fields
+    }
+    ... on ErrorResult {
+      errorCode
+      message
+    }
+    // highlight-start
+    ... on OrderInterceptorError {
+      interceptorError
+    }
+    // highlight-end
+  }
+}
+```
+
+In the above example, the error message returned by the OrderInterceptor would be available in the `interceptorError` field.
+
+## Example: Min/max order quantity
+
+Let's say we want to allow ProductVariants to specify the minimum or maximum amount which may be added
+to an order. We can define custom fields to store this information and
+then use this custom field value to prevent an order line from being added to the order if the quantity
+is below the minimum.
+
+*Example*
+
+```ts
+import {
+  EntityHydrator,
+  Injector,
+  LanguageCode,
+  Order,
+  OrderInterceptor,
+  ProductVariant,
+  RequestContext,
+  TranslatorService,
+  VendurePlugin,
+  WillAddItemToOrderInput,
+  WillAdjustOrderLineInput,
+} from '@vendure/core';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+  interface CustomProductVariantFields {
+    minOrderQuantity?: number;
+    maxOrderQuantity?: number;
+  }
+}
+
+// This OrderInterceptor enforces minimum and maximum order quantities on ProductVariants.
+export class MinMaxOrderInterceptor implements OrderInterceptor {
+ private entityHydrator: EntityHydrator;
+ private translatorService: TranslatorService;
+
+ init(injector: Injector) {
+   this.entityHydrator = injector.get(EntityHydrator);
+   this.translatorService = injector.get(TranslatorService);
+ }
+
+ willAddItemToOrder(
+   ctx: RequestContext,
+   order: Order,
+   input: WillAddItemToOrderInput,
+ ): Promise<void | string> | void | string {
+   const { productVariant, quantity } = input;
+   const min = productVariant.customFields?.minOrderQuantity;
+   const max = productVariant.customFields?.maxOrderQuantity;
+   if (min && quantity < min) {
+     return this.minErrorMessage(ctx, productVariant, min);
+   }
+   if (max && quantity > max) {
+     return this.maxErrorMessage(ctx, productVariant, max);
+   }
+ }
+
+ willAdjustOrderLine(
+   ctx: RequestContext,
+   order: Order,
+   input: WillAdjustOrderLineInput,
+ ): Promise<void | string> | void | string {
+   const { orderLine, quantity } = input;
+   const min = orderLine.productVariant.customFields?.minOrderQuantity;
+   const max = orderLine.productVariant.customFields?.maxOrderQuantity;
+   if (min && quantity < min) {
+     return this.minErrorMessage(ctx, orderLine.productVariant, min);
+   }
+   if (max && quantity > max) {
+     return this.maxErrorMessage(ctx, orderLine.productVariant, max);
+   }
+ }
+
+ private async minErrorMessage(ctx: RequestContext, variant: ProductVariant, min: number) {
+   const variantName = await this.getTranslatedVariantName(ctx, variant);
+   return `Minimum order quantity for "${variantName}" is ${min}`;
+ }
+
+ private async maxErrorMessage(ctx: RequestContext, variant: ProductVariant, max: number) {
+   const variantName = await this.getTranslatedVariantName(ctx, variant);
+   return `Maximum order quantity for "${variantName}" is ${max}`;
+ }
+
+ private async getTranslatedVariantName(ctx: RequestContext, variant: ProductVariant) {
+   await this.entityHydrator.hydrate(ctx, variant, { relations: ['translations'] });
+   const translated = this.translatorService.translate(variant, ctx);
+   return translated.name;
+ }
+}
+
+// This plugin enforces minimum and maximum order quantities on ProductVariants.
+// It adds two new custom fields to ProductVariant:
+// - minOrderQuantity
+// - maxOrderQuantity
+//
+// It also adds an OrderInterceptor which enforces these limits.
+@VendurePlugin({
+ configuration: config => {
+ // Here we add the custom fields to the ProductVariant entity
+   config.customFields.ProductVariant.push({
+     type: 'int',
+     min: 0,
+     name: 'minOrderQuantity',
+     label: [{ languageCode: LanguageCode.en, value: 'Minimum order quantity' }],
+     nullable: true,
+   });
+   config.customFields.ProductVariant.push({
+     type: 'int',
+     min: 0,
+     name: 'maxOrderQuantity',
+     label: [{ languageCode: LanguageCode.en, value: 'Maximum order quantity' }],
+     nullable: true,
+   });
+
+   // Here we add the MinMaxOrderInterceptor to the orderInterceptors array
+   config.orderOptions.orderInterceptors.push(new MinMaxOrderInterceptor());
+   return config;
+ },
+})
+export class OrderQuantityLimitsPlugin {}
+```
+
+```ts title="Signature"
+interface OrderInterceptor extends InjectableStrategy {
+    willAddItemToOrder?(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAddItemToOrderInput,
+    ): Promise<void | string> | void | string;
+    willAdjustOrderLine?(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAdjustOrderLineInput,
+    ): Promise<void | string> | void | string;
+    willRemoveItemFromOrder?(
+        ctx: RequestContext,
+        order: Order,
+        orderLine: OrderLine,
+    ): Promise<void | string> | void | string;
+}
+```
+* Extends: <code><a href='/reference/typescript-api/common/injectable-strategy#injectablestrategy'>InjectableStrategy</a></code>
+
+
+
+<div className="members-wrapper">
+
+### willAddItemToOrder
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>, input: WillAddItemToOrderInput) => Promise&#60;void | string&#62; | void | string`}   />
+
+Called when a new item is about to be added to the order,
+as in the `addItemToOrder` mutation or the `addItemToOrder()` / `addItemsToOrder()` method
+of the <a href='/reference/typescript-api/services/order-service#orderservice'>OrderService</a>.
+### willAdjustOrderLine
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>, input: WillAdjustOrderLineInput) => Promise&#60;void | string&#62; | void | string`}   />
+
+Called when an existing order line is about to be adjusted,
+as in the `adjustOrderLine` mutation or the `adjustOrderLine()` / `adjustOrderLines()` method
+of the <a href='/reference/typescript-api/services/order-service#orderservice'>OrderService</a>.
+### willRemoveItemFromOrder
+
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>, orderLine: <a href='/reference/typescript-api/entities/order-line#orderline'>OrderLine</a>) => Promise&#60;void | string&#62; | void | string`}   />
+
+Called when an item is about to be removed from the order,
+as in the `removeItemFromOrder` mutation or the `removeItemFromOrder()` / `removeItemsFromOrder()` method
+of the <a href='/reference/typescript-api/services/order-service#orderservice'>OrderService</a>.
+
+
+</div>

+ 2 - 1
docs/docs/reference/typescript-api/orders/order-item-price-calculation-strategy.md

@@ -87,7 +87,8 @@ Receives the ProductVariant to be added to the Order as well as any OrderLine cu
 the price for a single unit.
 
 Note: if you have any `relation` type custom fields defined on the OrderLine entity, they will only be
-passed in to this method if they are set to `eager: true`. Otherwise, you can use the <a href='/reference/typescript-api/data-access/entity-hydrator#entityhydrator'>EntityHydrator</a>to join the missing relations.
+passed in to this method if they are set to `eager: true`. Otherwise, you can use the <a href='/reference/typescript-api/data-access/entity-hydrator#entityhydrator'>EntityHydrator</a>
+to join the missing relations.
 
 Note: the `quantity` argument was added in v2.0.0
 

+ 8 - 2
docs/docs/reference/typescript-api/orders/order-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## OrderOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="484" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="485" packageName="@vendure/core" />
 
 
 
@@ -31,6 +31,7 @@ interface OrderOptions {
     activeOrderStrategy?: ActiveOrderStrategy<any> | Array<ActiveOrderStrategy<any>>;
     orderSellerStrategy?: OrderSellerStrategy;
     guestCheckoutStrategy?: GuestCheckoutStrategy;
+    orderInterceptors?: OrderInterceptor[];
 }
 ```
 
@@ -135,9 +136,14 @@ returns an Order will be used.
 Defines how Orders will be split amongst multiple Channels in a multivendor scenario.
 ### guestCheckoutStrategy
 
-<MemberInfo kind="property" type={`<a href='/reference/typescript-api/orders/guest-checkout-strategy#guestcheckoutstrategy'>GuestCheckoutStrategy</a>`} default={`<a href='/reference/typescript-api/orders/default-guest-checkout-strategy#defaultguestcheckoutstrategy'>DefaultGuestCheckoutStrategy</a>`}  since="2.0.0"  />
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/orders/guest-checkout-strategy#guestcheckoutstrategy'>GuestCheckoutStrategy</a>`} default={`<a href='/reference/typescript-api/orders/default-guest-checkout-strategy#defaultguestcheckoutstrategy'>DefaultGuestCheckoutStrategy</a>`}   />
 
 Defines how we deal with guest checkouts.
+### orderInterceptors
+
+<MemberInfo kind="property" type={`<a href='/reference/typescript-api/orders/order-interceptor#orderinterceptor'>OrderInterceptor</a>[]`} default={`[]`}  since="3.1.0"  />
+
+An array of <a href='/reference/typescript-api/orders/order-interceptor#orderinterceptor'>OrderInterceptor</a>s which can be used to modify the behavior of the Order process.
 
 
 </div>

+ 1 - 1
docs/docs/reference/typescript-api/payment/payment-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## PaymentOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="827" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="837" packageName="@vendure/core" />
 
 Defines payment-related options in the <a href='/reference/typescript-api/configuration/vendure-config#vendureconfig'>VendureConfig</a>.
 

+ 1 - 1
docs/docs/reference/typescript-api/products-stock/catalog-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## CatalogOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="676" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="686" packageName="@vendure/core" />
 
 Options related to products and collections.
 

+ 1 - 1
docs/docs/reference/typescript-api/promotions/promotion-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## PromotionOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="738" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="748" packageName="@vendure/core" />
 
 
 

+ 13 - 13
docs/docs/reference/typescript-api/request/request-context-service.md

@@ -17,14 +17,14 @@ Creates new <a href='/reference/typescript-api/request/request-context#requestco
 
 ```ts title="Signature"
 class RequestContextService {
-    create(config: {
-        req?: Request;
-        apiType: ApiType;
-        channelOrToken?: Channel | string;
-        languageCode?: LanguageCode;
-        currencyCode?: CurrencyCode;
-        user?: User;
-        activeOrderId?: ID;
+    create(config: {
+        req?: Request;
+        apiType: ApiType;
+        channelOrToken?: Channel | string;
+        languageCode?: LanguageCode;
+        currencyCode?: CurrencyCode;
+        user?: User;
+        activeOrderId?: ID;
     }) => Promise<RequestContext>;
     fromRequest(req: Request, info?: GraphQLResolveInfo, requiredPermissions?: Permission[], session?: CachedSession) => Promise<RequestContext>;
 }
@@ -34,17 +34,17 @@ class RequestContextService {
 
 ### create
 
-<MemberInfo kind="method" type={`(config: {         req?: Request;         apiType: <a href='/reference/typescript-api/request/api-type#apitype'>ApiType</a>;         channelOrToken?: <a href='/reference/typescript-api/entities/channel#channel'>Channel</a> | string;         languageCode?: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         currencyCode?: <a href='/reference/typescript-api/common/currency-code#currencycode'>CurrencyCode</a>;         user?: <a href='/reference/typescript-api/entities/user#user'>User</a>;         activeOrderId?: <a href='/reference/typescript-api/common/id#id'>ID</a>;     }) => Promise&#60;<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>&#62;`}  since="1.5.0"  />
+<MemberInfo kind="method" type={`(config: {
         req?: Request;
         apiType: <a href='/reference/typescript-api/request/api-type#apitype'>ApiType</a>;
         channelOrToken?: <a href='/reference/typescript-api/entities/channel#channel'>Channel</a> | string;
         languageCode?: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         currencyCode?: <a href='/reference/typescript-api/common/currency-code#currencycode'>CurrencyCode</a>;
         user?: <a href='/reference/typescript-api/entities/user#user'>User</a>;
         activeOrderId?: <a href='/reference/typescript-api/common/id#id'>ID</a>;
     }) => Promise&#60;<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>&#62;`}  since="1.5.0"  />
 
-Creates a RequestContext based on the config provided. This can be useful when interacting
-with services outside the request-response cycle, for example in stand-alone scripts or in
+Creates a RequestContext based on the config provided. This can be useful when interacting
+with services outside the request-response cycle, for example in stand-alone scripts or in
 worker jobs.
 ### fromRequest
 
 <MemberInfo kind="method" type={`(req: Request, info?: GraphQLResolveInfo, requiredPermissions?: <a href='/reference/typescript-api/common/permission#permission'>Permission</a>[], session?: <a href='/reference/typescript-api/auth/session-cache-strategy#cachedsession'>CachedSession</a>) => Promise&#60;<a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>&#62;`}   />
 
-Creates a new RequestContext based on an Express request object. This is used internally
-in the API layer by the AuthGuard, and creates the RequestContext which is then passed
+Creates a new RequestContext based on an Express request object. This is used internally
+in the API layer by the AuthGuard, and creates the RequestContext which is then passed
 to all resolvers & controllers.
 
 

+ 11 - 11
docs/docs/reference/typescript-api/service-helpers/slug-validator.md

@@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 <GenerationInfo sourceFile="packages/core/src/service/helpers/slug-validator/slug-validator.ts" sourceLine="44" packageName="@vendure/core" />
 
-Used to validate slugs to ensure they are URL-safe and unique. Designed to be used with translatable
+Used to validate slugs to ensure they are URL-safe and unique. Designed to be used with translatable
 entities such as <a href='/reference/typescript-api/entities/product#product'>Product</a> and <a href='/reference/typescript-api/entities/collection#collection'>Collection</a>.
 
 ```ts title="Signature"
@@ -49,10 +49,10 @@ class SlugValidator {
 ```ts title="Signature"
 type InputWithSlug = {
     id?: ID | null;
-    translations?: Array<{
-        id?: ID | null;
-        languageCode: LanguageCode;
-        slug?: string | null;
+    translations?: Array<{
+        id?: ID | null;
+        languageCode: LanguageCode;
+        slug?: string | null;
     }> | null;
 }
 ```
@@ -66,7 +66,7 @@ type InputWithSlug = {
 
 ### translations
 
-<MemberInfo kind="property" type={`Array&#60;{         id?: <a href='/reference/typescript-api/common/id#id'>ID</a> | null;         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;         slug?: string | null;     }&#62; | null`}   />
+<MemberInfo kind="property" type={`Array&#60;{
         id?: <a href='/reference/typescript-api/common/id#id'>ID</a> | null;
         languageCode: <a href='/reference/typescript-api/common/language-code#languagecode'>LanguageCode</a>;
         slug?: string | null;
     }&#62; | null`}   />
 
 
 
@@ -81,10 +81,10 @@ type InputWithSlug = {
 
 
 ```ts title="Signature"
-type TranslationEntity = VendureEntity & {
-    id: ID;
-    languageCode: LanguageCode;
-    slug: string;
-    base: any;
+type TranslationEntity = VendureEntity & {
+    id: ID;
+    languageCode: LanguageCode;
+    slug: string;
+    base: any;
 }
 ```

+ 25 - 25
docs/docs/reference/typescript-api/services/customer-service.md

@@ -31,8 +31,8 @@ class CustomerService {
     refreshVerificationToken(ctx: RequestContext, emailAddress: string) => Promise<void>;
     verifyCustomerEmailAddress(ctx: RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>>;
     requestPasswordReset(ctx: RequestContext, emailAddress: string) => Promise<void>;
-    resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
-        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
+    resetPassword(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
+        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     >;
     requestUpdateEmailAddress(ctx: RequestContext, userId: ID, newEmailAddress: string) => Promise<boolean | EmailAddressConflictError>;
     updateEmailAddress(ctx: RequestContext, token: string) => Promise<boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError>;
@@ -69,8 +69,8 @@ class CustomerService {
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, userId: <a href='/reference/typescript-api/common/id#id'>ID</a>, filterOnChannel:  = true) => Promise&#60;<a href='/reference/typescript-api/entities/customer#customer'>Customer</a> | undefined&#62;`}   />
 
-Returns the Customer entity associated with the given userId, if one exists.
-Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned
+Returns the Customer entity associated with the given userId, if one exists.
+Setting `filterOnChannel` to `true` will limit the results to Customers which are assigned
 to the current active Channel only.
 ### findAddressesByCustomerId
 
@@ -86,13 +86,13 @@ Returns a list of all <a href='/reference/typescript-api/entities/customer-group
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: CreateCustomerInput, password?: string) => Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;CreateCustomerResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 
-Creates a new Customer, including creation of a new User with the special `customer` Role.
-
-If the `password` argument is specified, the Customer will be immediately verified. If not,
-then an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a> is published, so that the customer can have their
-email address verified and set their password in a later step using the `verifyCustomerEmailAddress()`
-method.
-
+Creates a new Customer, including creation of a new User with the special `customer` Role.
+
+If the `password` argument is specified, the Customer will be immediately verified. If not,
+then an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a> is published, so that the customer can have their
+email address verified and set their password in a later step using the `verifyCustomerEmailAddress()`
+method.
+
 This method is intended to be used in admin-created Customer flows.
 ### update
 
@@ -113,47 +113,47 @@ This method is intended to be used in admin-created Customer flows.
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, input: RegisterCustomerInput) => Promise&#60;RegisterCustomerAccountResult | EmailAddressConflictError | PasswordValidationError&#62;`}   />
 
-Registers a new Customer account with the <a href='/reference/typescript-api/auth/native-authentication-strategy#nativeauthenticationstrategy'>NativeAuthenticationStrategy</a> and starts
-the email verification flow (unless <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification` is set to `false`)
-by publishing an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
-
+Registers a new Customer account with the <a href='/reference/typescript-api/auth/native-authentication-strategy#nativeauthenticationstrategy'>NativeAuthenticationStrategy</a> and starts
+the email verification flow (unless <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification` is set to `false`)
+by publishing an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
+
 This method is intended to be used in storefront Customer-creation flows.
 ### refreshVerificationToken
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 
-Refreshes a stale email address verification token by generating a new one and
+Refreshes a stale email address verification token by generating a new one and
 publishing a <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>.
 ### verifyCustomerEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, verificationToken: string, password?: string) => Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;VerifyCustomerAccountResult, <a href='/reference/typescript-api/entities/customer#customer'>Customer</a>&#62;&#62;`}   />
 
-Given a valid verification token which has been published in an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>, this
+Given a valid verification token which has been published in an <a href='/reference/typescript-api/events/event-types#accountregistrationevent'>AccountRegistrationEvent</a>, this
 method is used to set the Customer as `verified` as part of the account registration flow.
 ### requestPasswordReset
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;void&#62;`}   />
 
-Publishes a new <a href='/reference/typescript-api/events/event-types#passwordresetevent'>PasswordResetEvent</a> for the given email address. This event creates
+Publishes a new <a href='/reference/typescript-api/events/event-types#passwordresetevent'>PasswordResetEvent</a> for the given email address. This event creates
 a token which can be used in the `resetPassword()` method.
 ### resetPassword
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError     &#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;
         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     &#62;`}   />
 
-Given a valid password reset token created by a call to the `requestPasswordReset()` method,
+Given a valid password reset token created by a call to the `requestPasswordReset()` method,
 this method will change the Customer's password to that given as the `password` argument.
 ### requestUpdateEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, userId: <a href='/reference/typescript-api/common/id#id'>ID</a>, newEmailAddress: string) => Promise&#60;boolean | EmailAddressConflictError&#62;`}   />
 
-Publishes a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a> for the given User. This event contains a token
-which is then used in the `updateEmailAddress()` method to change the email address of the User &
+Publishes a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a> for the given User. This event contains a token
+which is then used in the `updateEmailAddress()` method to change the email address of the User &
 Customer.
 ### updateEmailAddress
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, token: string) => Promise&#60;boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError&#62;`}   />
 
-Given a valid email update token published in a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a>, this method
+Given a valid email update token published in a <a href='/reference/typescript-api/events/event-types#identifierchangerequestevent'>IdentifierChangeRequestEvent</a>, this method
 will update the Customer & User email address.
 ### createOrUpdate
 
@@ -184,8 +184,8 @@ Creates a new <a href='/reference/typescript-api/entities/address#address'>Addre
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, order: <a href='/reference/typescript-api/entities/order#order'>Order</a>) => `}   />
 
-If the Customer associated with the given Order does not yet have any Addresses,
-this method will create new Address(es) based on the Order's shipping & billing
+If the Customer associated with the given Order does not yet have any Addresses,
+this method will create new Address(es) based on the Order's shipping & billing
 addresses.
 ### addNoteToCustomer
 

+ 11 - 11
docs/docs/reference/typescript-api/services/fulfillment-service.md

@@ -21,14 +21,14 @@ class FulfillmentService {
     create(ctx: RequestContext, orders: Order[], lines: OrderLineInput[], handler: ConfigurableOperationInput) => Promise<Fulfillment | InvalidFulfillmentHandlerError | CreateFulfillmentError>;
     getFulfillmentLines(ctx: RequestContext, id: ID) => Promise<FulfillmentLine[]>;
     getFulfillmentsLinesForOrderLine(ctx: RequestContext, orderLineId: ID, relations: RelationPaths<FulfillmentLine> = []) => Promise<FulfillmentLine[]>;
-    transitionToState(ctx: RequestContext, fulfillmentId: ID, state: FulfillmentState) => Promise<
-        | {
-              fulfillment: Fulfillment;
-              orders: Order[];
-              fromState: FulfillmentState;
-              toState: FulfillmentState;
-          }
-        | FulfillmentStateTransitionError
+    transitionToState(ctx: RequestContext, fulfillmentId: ID, state: FulfillmentState) => Promise<
+        | {
+              fulfillment: Fulfillment;
+              orders: Order[];
+              fromState: FulfillmentState;
+              toState: FulfillmentState;
+          }
+        | FulfillmentStateTransitionError
     >;
     getNextStates(fulfillment: Fulfillment) => readonly FulfillmentState[];
 }
@@ -45,7 +45,7 @@ class FulfillmentService {
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[], lines: OrderLineInput[], handler: ConfigurableOperationInput) => Promise&#60;<a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a> | InvalidFulfillmentHandlerError | CreateFulfillmentError&#62;`}   />
 
-Creates a new Fulfillment for the given Orders and OrderItems, using the specified
+Creates a new Fulfillment for the given Orders and OrderItems, using the specified
 <a href='/reference/typescript-api/fulfillment/fulfillment-handler#fulfillmenthandler'>FulfillmentHandler</a>.
 ### getFulfillmentLines
 
@@ -59,9 +59,9 @@ Creates a new Fulfillment for the given Orders and OrderItems, using the specifi
 
 ### transitionToState
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, fulfillmentId: <a href='/reference/typescript-api/common/id#id'>ID</a>, state: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>) => Promise&#60;         | {               fulfillment: <a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a>;               orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[];               fromState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;               toState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;           }         | FulfillmentStateTransitionError     &#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, fulfillmentId: <a href='/reference/typescript-api/common/id#id'>ID</a>, state: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>) => Promise&#60;
         | {
               fulfillment: <a href='/reference/typescript-api/entities/fulfillment#fulfillment'>Fulfillment</a>;
               orders: <a href='/reference/typescript-api/entities/order#order'>Order</a>[];
               fromState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;
               toState: <a href='/reference/typescript-api/fulfillment/fulfillment-state#fulfillmentstate'>FulfillmentState</a>;
           }
         | FulfillmentStateTransitionError
     &#62;`}   />
 
-Transitions the specified Fulfillment to a new state and upon successful transition
+Transitions the specified Fulfillment to a new state and upon successful transition
 publishes a <a href='/reference/typescript-api/events/event-types#fulfillmentstatetransitionevent'>FulfillmentStateTransitionEvent</a>.
 ### getNextStates
 

+ 1 - 1
docs/docs/reference/typescript-api/services/order-service.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## OrderService
 
-<GenerationInfo sourceFile="packages/core/src/service/services/order.service.ts" sourceLine="138" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/service/services/order.service.ts" sourceLine="139" packageName="@vendure/core" />
 
 Contains methods relating to <a href='/reference/typescript-api/entities/order#order'>Order</a> entities.
 

+ 5 - 5
docs/docs/reference/typescript-api/services/payment-method-service.md

@@ -28,10 +28,10 @@ class PaymentMethodService {
     getPaymentMethodEligibilityCheckers(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getPaymentMethodHandlers(ctx: RequestContext) => ConfigurableOperationDefinition[];
     getEligiblePaymentMethods(ctx: RequestContext, order: Order) => Promise<PaymentMethodQuote[]>;
-    getMethodAndOperations(ctx: RequestContext, method: string) => Promise<{
-        paymentMethod: PaymentMethod;
-        handler: PaymentMethodHandler;
-        checker: PaymentMethodEligibilityChecker | null;
+    getMethodAndOperations(ctx: RequestContext, method: string) => Promise<{
+        paymentMethod: PaymentMethod;
+        handler: PaymentMethodHandler;
+        checker: PaymentMethodEligibilityChecker | null;
     }>;
 }
 ```
@@ -95,7 +95,7 @@ class PaymentMethodService {
 
 ### getMethodAndOperations
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, method: string) => Promise&#60;{         paymentMethod: <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a>;         handler: <a href='/reference/typescript-api/payment/payment-method-handler#paymentmethodhandler'>PaymentMethodHandler</a>;         checker: <a href='/reference/typescript-api/payment/payment-method-eligibility-checker#paymentmethodeligibilitychecker'>PaymentMethodEligibilityChecker</a> | null;     }&#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, method: string) => Promise&#60;{
         paymentMethod: <a href='/reference/typescript-api/entities/payment-method#paymentmethod'>PaymentMethod</a>;
         handler: <a href='/reference/typescript-api/payment/payment-method-handler#paymentmethodhandler'>PaymentMethodHandler</a>;
         checker: <a href='/reference/typescript-api/payment/payment-method-eligibility-checker#paymentmethodeligibilitychecker'>PaymentMethodEligibilityChecker</a> | null;
     }&#62;`}   />
 
 
 

+ 21 - 21
docs/docs/reference/typescript-api/services/user-service.md

@@ -27,15 +27,15 @@ class UserService {
     setVerificationToken(ctx: RequestContext, user: User) => Promise<User>;
     verifyUserByToken(ctx: RequestContext, verificationToken: string, password?: string) => Promise<ErrorResultUnion<VerifyCustomerAccountResult, User>>;
     setPasswordResetToken(ctx: RequestContext, emailAddress: string) => Promise<User | undefined>;
-    resetPasswordByToken(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
-        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
+    resetPasswordByToken(ctx: RequestContext, passwordResetToken: string, password: string) => Promise<
+        User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     >;
     changeUserAndNativeIdentifier(ctx: RequestContext, userId: ID, newIdentifier: string) => ;
     setIdentifierChangeToken(ctx: RequestContext, user: User) => Promise<User>;
-    changeIdentifierByToken(ctx: RequestContext, token: string) => Promise<
-        | { user: User; oldIdentifier: string }
-        | IdentifierChangeTokenInvalidError
-        | IdentifierChangeTokenExpiredError
+    changeIdentifierByToken(ctx: RequestContext, token: string) => Promise<
+        | { user: User; oldIdentifier: string }
+        | IdentifierChangeTokenInvalidError
+        | IdentifierChangeTokenExpiredError
     >;
     updatePassword(ctx: RequestContext, userId: ID, currentPassword: string, newPassword: string) => Promise<boolean | InvalidCredentialsError | PasswordValidationError>;
 }
@@ -67,8 +67,8 @@ Creates a new User with the special `customer` Role and using the <a href='/refe
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, user: <a href='/reference/typescript-api/entities/user#user'>User</a>, identifier: string, password?: string) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordValidationError&#62;`}   />
 
-Adds a new <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> to the User. If the <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification`
-is set to `true` (as is the default), the User will be marked as unverified until the email verification
+Adds a new <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> to the User. If the <a href='/reference/typescript-api/auth/auth-options#authoptions'>AuthOptions</a> `requireVerification`
+is set to `true` (as is the default), the User will be marked as unverified until the email verification
 flow is completed.
 ### createAdminUser
 
@@ -84,47 +84,47 @@ Creates a new verified User using the <a href='/reference/typescript-api/auth/na
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, user: <a href='/reference/typescript-api/entities/user#user'>User</a>) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a>&#62;`}   />
 
-Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `verificationToken` as part of the User email verification
+Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `verificationToken` as part of the User email verification
 flow.
 ### verifyUserByToken
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, verificationToken: string, password?: string) => Promise&#60;<a href='/reference/typescript-api/errors/error-result-union#errorresultunion'>ErrorResultUnion</a>&#60;VerifyCustomerAccountResult, <a href='/reference/typescript-api/entities/user#user'>User</a>&#62;&#62;`}   />
 
-Verifies a verificationToken by looking for a User which has previously had it set using the
-`setVerificationToken()` method, and checks that the token is valid and has not expired.
-
+Verifies a verificationToken by looking for a User which has previously had it set using the
+`setVerificationToken()` method, and checks that the token is valid and has not expired.
+
 If valid, the User will be set to `verified: true`.
 ### setPasswordResetToken
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, emailAddress: string) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a> | undefined&#62;`}   />
 
-Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `passwordResetToken` as part of the User password reset
+Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `passwordResetToken` as part of the User password reset
 flow.
 ### resetPasswordByToken
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError     &#62;`}   />
-
-Verifies a passwordResetToken by looking for a User which has previously had it set using the
-`setPasswordResetToken()` method, and checks that the token is valid and has not expired.
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, passwordResetToken: string, password: string) => Promise&#60;
         <a href='/reference/typescript-api/entities/user#user'>User</a> | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError | PasswordValidationError
     &#62;`}   />
 
+Verifies a passwordResetToken by looking for a User which has previously had it set using the
+`setPasswordResetToken()` method, and checks that the token is valid and has not expired.
+
 If valid, the User's credentials will be updated with the new password.
 ### changeUserAndNativeIdentifier
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, userId: <a href='/reference/typescript-api/common/id#id'>ID</a>, newIdentifier: string) => `}   />
 
-Changes the User identifier without an email verification step, so this should be only used when
+Changes the User identifier without an email verification step, so this should be only used when
 an Administrator is setting a new email address.
 ### setIdentifierChangeToken
 
 <MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, user: <a href='/reference/typescript-api/entities/user#user'>User</a>) => Promise&#60;<a href='/reference/typescript-api/entities/user#user'>User</a>&#62;`}   />
 
-Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `identifierChangeToken` as part of the User email address change
+Sets the <a href='/reference/typescript-api/entities/authentication-method#nativeauthenticationmethod'>NativeAuthenticationMethod</a> `identifierChangeToken` as part of the User email address change
 flow.
 ### changeIdentifierByToken
 
-<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, token: string) => Promise&#60;         | { user: <a href='/reference/typescript-api/entities/user#user'>User</a>; oldIdentifier: string }         | IdentifierChangeTokenInvalidError         | IdentifierChangeTokenExpiredError     &#62;`}   />
+<MemberInfo kind="method" type={`(ctx: <a href='/reference/typescript-api/request/request-context#requestcontext'>RequestContext</a>, token: string) => Promise&#60;
         | { user: <a href='/reference/typescript-api/entities/user#user'>User</a>; oldIdentifier: string }
         | IdentifierChangeTokenInvalidError
         | IdentifierChangeTokenExpiredError
     &#62;`}   />
 
-Changes the User identifier as part of the storefront flow used by Customers to set a
+Changes the User identifier as part of the storefront flow used by Customers to set a
 new email address, with the token previously set using the `setIdentifierChangeToken()` method.
 ### updatePassword
 

+ 1 - 1
docs/docs/reference/typescript-api/shipping/shipping-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## ShippingOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="754" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="764" packageName="@vendure/core" />
 
 
 

+ 1 - 1
docs/docs/reference/typescript-api/tax/tax-options.md

@@ -11,7 +11,7 @@ import MemberDescription from '@site/src/components/MemberDescription';
 
 ## TaxOptions
 
-<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="867" packageName="@vendure/core" />
+<GenerationInfo sourceFile="packages/core/src/config/vendure-config.ts" sourceLine="877" packageName="@vendure/core" />
 
 
 

+ 35 - 10
docs/docs/reference/typescript-api/testing/simple-graph-qlclient.md

@@ -27,10 +27,10 @@ class SimpleGraphQLClient {
     asUserWithCredentials(username: string, password: string) => ;
     asSuperAdmin() => ;
     asAnonymousUser() => ;
-    fileUploadMutation(options: {
-        mutation: DocumentNode;
-        filePaths: string[];
-        mapVariables: (filePaths: string[]) => any;
+    fileUploadMutation(options: {
+        mutation: DocumentNode;
+        filePaths: string[];
+        mapVariables: (filePaths: string[]) => any;
     }) => Promise<any>;
 }
 ```
@@ -66,8 +66,8 @@ Performs both query and mutation operations.
 
 <MemberInfo kind="method" type={`(url: string, options: RequestInit = {}) => Promise&#60;Response&#62;`}   />
 
-Performs a raw HTTP request to the given URL, but also includes the authToken & channelToken
-headers if they have been set. Useful for testing non-GraphQL endpoints, e.g. for plugins
+Performs a raw HTTP request to the given URL, but also includes the authToken & channelToken
+headers if they have been set. Useful for testing non-GraphQL endpoints, e.g. for plugins
 which make use of REST controllers.
 ### queryStatus
 
@@ -91,12 +91,37 @@ Logs in as the SuperAdmin user.
 Logs out so that the client is then treated as an anonymous user.
 ### fileUploadMutation
 
-<MemberInfo kind="method" type={`(options: {
         mutation: DocumentNode;
         filePaths: string[];
         mapVariables: (filePaths: string[]) =&#62; any;
     }) => Promise&#60;any&#62;`}   />
+<MemberInfo kind="method" type={`(options: {         mutation: DocumentNode;         filePaths: string[];         mapVariables: (filePaths: string[]) =&#62; any;     }) => Promise&#60;any&#62;`}   />
+
+Perform a file upload mutation.
+
+Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
 
-Perform a file upload mutation.
-
-Upload spec: https://github.com/jaydenseric/graphql-multipart-request-spec
 Discussion of issue: https://github.com/jaydenseric/apollo-upload-client/issues/32
 
+*Example*
+
+```ts
+// Testing a custom mutation:
+const result = await client.fileUploadMutation({
+  mutation: gql`
+    mutation AddSellerImages($input: AddSellerImagesInput!) {
+      addSellerImages(input: $input) {
+        id
+        name
+      }
+    }
+  `,
+  filePaths: ['./images/profile-picture.jpg', './images/logo.png'],
+  mapVariables: () => ({
+    name: "George's Pans",
+    profilePicture: null,  // corresponds to filePaths[0]
+    branding: {
+      logo: null  // corresponds to filePaths[1]
+    }
+  })
+});
+```
+
 
 </div>

File diff suppressed because it is too large
+ 28 - 25
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 349 - 260
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,263 +1,352 @@
 /* eslint-disable */
 
-export interface PossibleTypesResultData {
-    possibleTypes: {
-        [key: string]: string[];
-    };
-}
-const result: PossibleTypesResultData = {
-    possibleTypes: {
-        AddFulfillmentToOrderResult: [
-            'CreateFulfillmentError',
-            'EmptyOrderLineSelectionError',
-            'Fulfillment',
-            'FulfillmentStateTransitionError',
-            'InsufficientStockOnHandError',
-            'InvalidFulfillmentHandlerError',
-            'ItemsAlreadyFulfilledError',
-        ],
-        AddManualPaymentToOrderResult: ['ManualPaymentStateError', 'Order'],
-        ApplyCouponCodeResult: [
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'Order',
-        ],
-        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
-        CancelOrderResult: [
-            'CancelActiveOrderError',
-            'EmptyOrderLineSelectionError',
-            'MultipleOrderError',
-            'Order',
-            'OrderStateTransitionError',
-            'QuantityTooGreatError',
-        ],
-        CancelPaymentResult: ['CancelPaymentError', 'Payment', 'PaymentStateTransitionError'],
-        CreateAssetResult: ['Asset', 'MimeTypeError'],
-        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        CreatePromotionResult: ['MissingConditionsError', 'Promotion'],
-        CustomField: [
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'LocaleTextCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'StringCustomFieldConfig',
-            'StructCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        CustomFieldConfig: [
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'LocaleTextCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'StringCustomFieldConfig',
-            'StructCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        DuplicateEntityResult: ['DuplicateEntityError', 'DuplicateEntitySuccess'],
-        ErrorResult: [
-            'AlreadyRefundedError',
-            'CancelActiveOrderError',
-            'CancelPaymentError',
-            'ChannelDefaultLanguageError',
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'CreateFulfillmentError',
-            'DuplicateEntityError',
-            'EmailAddressConflictError',
-            'EmptyOrderLineSelectionError',
-            'FacetInUseError',
-            'FulfillmentStateTransitionError',
-            'GuestCheckoutError',
-            'IneligibleShippingMethodError',
-            'InsufficientStockError',
-            'InsufficientStockOnHandError',
-            'InvalidCredentialsError',
-            'InvalidFulfillmentHandlerError',
-            'ItemsAlreadyFulfilledError',
-            'LanguageNotAvailableError',
-            'ManualPaymentStateError',
-            'MimeTypeError',
-            'MissingConditionsError',
-            'MultipleOrderError',
-            'NativeAuthStrategyError',
-            'NegativeQuantityError',
-            'NoActiveOrderError',
-            'NoChangesSpecifiedError',
-            'NothingToRefundError',
-            'OrderLimitError',
-            'OrderModificationError',
-            'OrderModificationStateError',
-            'OrderStateTransitionError',
-            'PaymentMethodMissingError',
-            'PaymentOrderMismatchError',
-            'PaymentStateTransitionError',
-            'ProductOptionInUseError',
-            'QuantityTooGreatError',
-            'RefundAmountError',
-            'RefundOrderStateError',
-            'RefundPaymentIdMissingError',
-            'RefundStateTransitionError',
-            'SettlePaymentError',
-        ],
-        ModifyOrderResult: [
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'IneligibleShippingMethodError',
-            'InsufficientStockError',
-            'NegativeQuantityError',
-            'NoChangesSpecifiedError',
-            'Order',
-            'OrderLimitError',
-            'OrderModificationStateError',
-            'PaymentMethodMissingError',
-            'RefundPaymentIdMissingError',
-        ],
-        NativeAuthenticationResult: ['CurrentUser', 'InvalidCredentialsError', 'NativeAuthStrategyError'],
-        Node: [
-            'Address',
-            'Administrator',
-            'Allocation',
-            'Asset',
-            'AuthenticationMethod',
-            'Cancellation',
-            'Channel',
-            'Collection',
-            'Country',
-            'Customer',
-            'CustomerGroup',
-            'Facet',
-            'FacetValue',
-            'Fulfillment',
-            'HistoryEntry',
-            'Job',
-            'Order',
-            'OrderLine',
-            'OrderModification',
-            'Payment',
-            'PaymentMethod',
-            'Product',
-            'ProductOption',
-            'ProductOptionGroup',
-            'ProductVariant',
-            'Promotion',
-            'Province',
-            'Refund',
-            'Release',
-            'Return',
-            'Role',
-            'Sale',
-            'Seller',
-            'ShippingMethod',
-            'StockAdjustment',
-            'StockLevel',
-            'StockLocation',
-            'Surcharge',
-            'Tag',
-            'TaxCategory',
-            'TaxRate',
-            'User',
-            'Zone',
-        ],
-        PaginatedList: [
-            'AdministratorList',
-            'AssetList',
-            'ChannelList',
-            'CollectionList',
-            'CountryList',
-            'CustomerGroupList',
-            'CustomerList',
-            'FacetList',
-            'FacetValueList',
-            'HistoryEntryList',
-            'JobList',
-            'OrderList',
-            'PaymentMethodList',
-            'ProductList',
-            'ProductVariantList',
-            'PromotionList',
-            'ProvinceList',
-            'RoleList',
-            'SellerList',
-            'ShippingMethodList',
-            'StockLocationList',
-            'TagList',
-            'TaxCategoryList',
-            'TaxRateList',
-            'ZoneList',
-        ],
-        RefundOrderResult: [
-            'AlreadyRefundedError',
-            'MultipleOrderError',
-            'NothingToRefundError',
-            'OrderStateTransitionError',
-            'PaymentOrderMismatchError',
-            'QuantityTooGreatError',
-            'Refund',
-            'RefundAmountError',
-            'RefundOrderStateError',
-            'RefundStateTransitionError',
-        ],
-        Region: ['Country', 'Province'],
-        RemoveFacetFromChannelResult: ['Facet', 'FacetInUseError'],
-        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
-        RemoveOrderItemsResult: ['Order', 'OrderModificationError'],
-        SearchResultPrice: ['PriceRange', 'SinglePrice'],
-        SetCustomerForDraftOrderResult: ['EmailAddressConflictError', 'Order'],
-        SetOrderShippingMethodResult: [
-            'IneligibleShippingMethodError',
-            'NoActiveOrderError',
-            'Order',
-            'OrderModificationError',
-        ],
-        SettlePaymentResult: [
-            'OrderStateTransitionError',
-            'Payment',
-            'PaymentStateTransitionError',
-            'SettlePaymentError',
-        ],
-        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
-        StockMovement: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
-        StockMovementItem: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
-        StructField: [
-            'BooleanStructFieldConfig',
-            'DateTimeStructFieldConfig',
-            'FloatStructFieldConfig',
-            'IntStructFieldConfig',
-            'StringStructFieldConfig',
-            'TextStructFieldConfig',
-        ],
-        StructFieldConfig: [
-            'BooleanStructFieldConfig',
-            'DateTimeStructFieldConfig',
-            'FloatStructFieldConfig',
-            'IntStructFieldConfig',
-            'StringStructFieldConfig',
-            'TextStructFieldConfig',
-        ],
-        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
-        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
-        TransitionPaymentToStateResult: ['Payment', 'PaymentStateTransitionError'],
-        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        UpdateGlobalSettingsResult: ['ChannelDefaultLanguageError', 'GlobalSettings'],
-        UpdateOrderItemsResult: [
-            'InsufficientStockError',
-            'NegativeQuantityError',
-            'Order',
-            'OrderLimitError',
-            'OrderModificationError',
-        ],
-        UpdatePromotionResult: ['MissingConditionsError', 'Promotion'],
-    },
+      export interface PossibleTypesResultData {
+        possibleTypes: {
+          [key: string]: string[]
+        }
+      }
+      const result: PossibleTypesResultData = {
+  "possibleTypes": {
+    "AddFulfillmentToOrderResult": [
+      "CreateFulfillmentError",
+      "EmptyOrderLineSelectionError",
+      "Fulfillment",
+      "FulfillmentStateTransitionError",
+      "InsufficientStockOnHandError",
+      "InvalidFulfillmentHandlerError",
+      "ItemsAlreadyFulfilledError"
+    ],
+    "AddManualPaymentToOrderResult": [
+      "ManualPaymentStateError",
+      "Order"
+    ],
+    "ApplyCouponCodeResult": [
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "Order"
+    ],
+    "AuthenticationResult": [
+      "CurrentUser",
+      "InvalidCredentialsError"
+    ],
+    "CancelOrderResult": [
+      "CancelActiveOrderError",
+      "EmptyOrderLineSelectionError",
+      "MultipleOrderError",
+      "Order",
+      "OrderStateTransitionError",
+      "QuantityTooGreatError"
+    ],
+    "CancelPaymentResult": [
+      "CancelPaymentError",
+      "Payment",
+      "PaymentStateTransitionError"
+    ],
+    "CreateAssetResult": [
+      "Asset",
+      "MimeTypeError"
+    ],
+    "CreateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "CreateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "CreatePromotionResult": [
+      "MissingConditionsError",
+      "Promotion"
+    ],
+    "CustomField": [
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "LocaleTextCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "StringCustomFieldConfig",
+      "StructCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "CustomFieldConfig": [
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "LocaleTextCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "StringCustomFieldConfig",
+      "StructCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "DuplicateEntityResult": [
+      "DuplicateEntityError",
+      "DuplicateEntitySuccess"
+    ],
+    "ErrorResult": [
+      "AlreadyRefundedError",
+      "CancelActiveOrderError",
+      "CancelPaymentError",
+      "ChannelDefaultLanguageError",
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "CreateFulfillmentError",
+      "DuplicateEntityError",
+      "EmailAddressConflictError",
+      "EmptyOrderLineSelectionError",
+      "FacetInUseError",
+      "FulfillmentStateTransitionError",
+      "GuestCheckoutError",
+      "IneligibleShippingMethodError",
+      "InsufficientStockError",
+      "InsufficientStockOnHandError",
+      "InvalidCredentialsError",
+      "InvalidFulfillmentHandlerError",
+      "ItemsAlreadyFulfilledError",
+      "LanguageNotAvailableError",
+      "ManualPaymentStateError",
+      "MimeTypeError",
+      "MissingConditionsError",
+      "MultipleOrderError",
+      "NativeAuthStrategyError",
+      "NegativeQuantityError",
+      "NoActiveOrderError",
+      "NoChangesSpecifiedError",
+      "NothingToRefundError",
+      "OrderInterceptorError",
+      "OrderLimitError",
+      "OrderModificationError",
+      "OrderModificationStateError",
+      "OrderStateTransitionError",
+      "PaymentMethodMissingError",
+      "PaymentOrderMismatchError",
+      "PaymentStateTransitionError",
+      "ProductOptionInUseError",
+      "QuantityTooGreatError",
+      "RefundAmountError",
+      "RefundOrderStateError",
+      "RefundPaymentIdMissingError",
+      "RefundStateTransitionError",
+      "SettlePaymentError"
+    ],
+    "ModifyOrderResult": [
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "IneligibleShippingMethodError",
+      "InsufficientStockError",
+      "NegativeQuantityError",
+      "NoChangesSpecifiedError",
+      "Order",
+      "OrderLimitError",
+      "OrderModificationStateError",
+      "PaymentMethodMissingError",
+      "RefundPaymentIdMissingError"
+    ],
+    "NativeAuthenticationResult": [
+      "CurrentUser",
+      "InvalidCredentialsError",
+      "NativeAuthStrategyError"
+    ],
+    "Node": [
+      "Address",
+      "Administrator",
+      "Allocation",
+      "Asset",
+      "AuthenticationMethod",
+      "Cancellation",
+      "Channel",
+      "Collection",
+      "Country",
+      "Customer",
+      "CustomerGroup",
+      "Facet",
+      "FacetValue",
+      "Fulfillment",
+      "HistoryEntry",
+      "Job",
+      "Order",
+      "OrderLine",
+      "OrderModification",
+      "Payment",
+      "PaymentMethod",
+      "Product",
+      "ProductOption",
+      "ProductOptionGroup",
+      "ProductVariant",
+      "Promotion",
+      "Province",
+      "Refund",
+      "Release",
+      "Return",
+      "Role",
+      "Sale",
+      "Seller",
+      "ShippingMethod",
+      "StockAdjustment",
+      "StockLevel",
+      "StockLocation",
+      "Surcharge",
+      "Tag",
+      "TaxCategory",
+      "TaxRate",
+      "User",
+      "Zone"
+    ],
+    "PaginatedList": [
+      "AdministratorList",
+      "AssetList",
+      "ChannelList",
+      "CollectionList",
+      "CountryList",
+      "CustomerGroupList",
+      "CustomerList",
+      "FacetList",
+      "FacetValueList",
+      "HistoryEntryList",
+      "JobList",
+      "OrderList",
+      "PaymentMethodList",
+      "ProductList",
+      "ProductVariantList",
+      "PromotionList",
+      "ProvinceList",
+      "RoleList",
+      "SellerList",
+      "ShippingMethodList",
+      "StockLocationList",
+      "TagList",
+      "TaxCategoryList",
+      "TaxRateList",
+      "ZoneList"
+    ],
+    "RefundOrderResult": [
+      "AlreadyRefundedError",
+      "MultipleOrderError",
+      "NothingToRefundError",
+      "OrderStateTransitionError",
+      "PaymentOrderMismatchError",
+      "QuantityTooGreatError",
+      "Refund",
+      "RefundAmountError",
+      "RefundOrderStateError",
+      "RefundStateTransitionError"
+    ],
+    "Region": [
+      "Country",
+      "Province"
+    ],
+    "RemoveFacetFromChannelResult": [
+      "Facet",
+      "FacetInUseError"
+    ],
+    "RemoveOptionGroupFromProductResult": [
+      "Product",
+      "ProductOptionInUseError"
+    ],
+    "RemoveOrderItemsResult": [
+      "Order",
+      "OrderInterceptorError",
+      "OrderModificationError"
+    ],
+    "SearchResultPrice": [
+      "PriceRange",
+      "SinglePrice"
+    ],
+    "SetCustomerForDraftOrderResult": [
+      "EmailAddressConflictError",
+      "Order"
+    ],
+    "SetOrderShippingMethodResult": [
+      "IneligibleShippingMethodError",
+      "NoActiveOrderError",
+      "Order",
+      "OrderModificationError"
+    ],
+    "SettlePaymentResult": [
+      "OrderStateTransitionError",
+      "Payment",
+      "PaymentStateTransitionError",
+      "SettlePaymentError"
+    ],
+    "SettleRefundResult": [
+      "Refund",
+      "RefundStateTransitionError"
+    ],
+    "StockMovement": [
+      "Allocation",
+      "Cancellation",
+      "Release",
+      "Return",
+      "Sale",
+      "StockAdjustment"
+    ],
+    "StockMovementItem": [
+      "Allocation",
+      "Cancellation",
+      "Release",
+      "Return",
+      "Sale",
+      "StockAdjustment"
+    ],
+    "StructField": [
+      "BooleanStructFieldConfig",
+      "DateTimeStructFieldConfig",
+      "FloatStructFieldConfig",
+      "IntStructFieldConfig",
+      "StringStructFieldConfig",
+      "TextStructFieldConfig"
+    ],
+    "StructFieldConfig": [
+      "BooleanStructFieldConfig",
+      "DateTimeStructFieldConfig",
+      "FloatStructFieldConfig",
+      "IntStructFieldConfig",
+      "StringStructFieldConfig",
+      "TextStructFieldConfig"
+    ],
+    "TransitionFulfillmentToStateResult": [
+      "Fulfillment",
+      "FulfillmentStateTransitionError"
+    ],
+    "TransitionOrderToStateResult": [
+      "Order",
+      "OrderStateTransitionError"
+    ],
+    "TransitionPaymentToStateResult": [
+      "Payment",
+      "PaymentStateTransitionError"
+    ],
+    "UpdateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "UpdateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "UpdateGlobalSettingsResult": [
+      "ChannelDefaultLanguageError",
+      "GlobalSettings"
+    ],
+    "UpdateOrderItemsResult": [
+      "InsufficientStockError",
+      "NegativeQuantityError",
+      "Order",
+      "OrderInterceptorError",
+      "OrderLimitError",
+      "OrderModificationError"
+    ],
+    "UpdatePromotionResult": [
+      "MissingConditionsError",
+      "Promotion"
+    ]
+  }
 };
-export default result;
+      export default result;
+    

File diff suppressed because it is too large
+ 435 - 457
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts


File diff suppressed because it is too large
+ 653 - 675
packages/common/src/generated-shop-types.ts


+ 20 - 17
packages/common/src/generated-types.ts

@@ -338,7 +338,6 @@ export type BooleanStructFieldConfig = StructField & {
   label?: Maybe<Array<LocalizedString>>;
   list: Scalars['Boolean']['output'];
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -1547,7 +1546,6 @@ export type DateTimeStructFieldConfig = StructField & {
   max?: Maybe<Scalars['String']['output']>;
   min?: Maybe<Scalars['String']['output']>;
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   step?: Maybe<Scalars['Int']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
@@ -1672,6 +1670,7 @@ export enum ErrorCode {
   NOTHING_TO_REFUND_ERROR = 'NOTHING_TO_REFUND_ERROR',
   NO_ACTIVE_ORDER_ERROR = 'NO_ACTIVE_ORDER_ERROR',
   NO_CHANGES_SPECIFIED_ERROR = 'NO_CHANGES_SPECIFIED_ERROR',
+  ORDER_INTERCEPTOR_ERROR = 'ORDER_INTERCEPTOR_ERROR',
   ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
   ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
   ORDER_MODIFICATION_STATE_ERROR = 'ORDER_MODIFICATION_STATE_ERROR',
@@ -1897,7 +1896,6 @@ export type FloatStructFieldConfig = StructField & {
   max?: Maybe<Scalars['Float']['output']>;
   min?: Maybe<Scalars['Float']['output']>;
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   step?: Maybe<Scalars['Float']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
@@ -2117,7 +2115,6 @@ export type IntStructFieldConfig = StructField & {
   max?: Maybe<Scalars['Int']['output']>;
   min?: Maybe<Scalars['Int']['output']>;
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   step?: Maybe<Scalars['Int']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
@@ -2980,7 +2977,7 @@ export type Mutation = {
   transitionPaymentToState: TransitionPaymentToStateResult;
   /** Unsets the billing address for a draft Order */
   unsetDraftOrderBillingAddress: Order;
-  /** Unsets the sthipping address for a draft Order */
+  /** Unsets the shipping address for a draft Order */
   unsetDraftOrderShippingAddress: Order;
   /** Update the active (currently logged-in) Administrator */
   updateActiveAdministrator: Administrator;
@@ -4050,6 +4047,14 @@ export type OrderFilterParameter = {
   updatedAt?: InputMaybe<DateOperators>;
 };
 
+/** Returned when an order operation is rejected by an OrderInterceptor method. */
+export type OrderInterceptorError = ErrorResult & {
+  __typename?: 'OrderInterceptorError';
+  errorCode: ErrorCode;
+  interceptorError: Scalars['String']['output'];
+  message: Scalars['String']['output'];
+};
+
 /** Returned when the maximum order size limit has been reached. */
 export type OrderLimitError = ErrorResult & {
   __typename?: 'OrderLimitError';
@@ -5472,18 +5477,19 @@ export type RefundLine = {
 };
 
 export type RefundOrderInput = {
-  adjustment: Scalars['Money']['input'];
+  /** @deprecated Use the `amount` field instead */
+  adjustment?: InputMaybe<Scalars['Money']['input']>;
   /**
-   * If an amount is specified, this value will be used to create a Refund rather than calculating the
-   * amount automatically. This was added in v2.2 and will be the preferred way to specify the refund
-   * amount in the future. The `lines`, `shipping` and `adjustment` fields will likely be removed in a future
-   * version.
+   * The amount to be refunded to this particular payment. This was introduced in v2.2.0 as the preferred way to specify the refund amount.
+   * Can be as much as the total amount of the payment minus the sum of all previous refunds.
    */
   amount?: InputMaybe<Scalars['Money']['input']>;
-  lines: Array<OrderLineInput>;
+  /** @deprecated Use the `amount` field instead */
+  lines?: InputMaybe<Array<OrderLineInput>>;
   paymentId: Scalars['ID']['input'];
   reason?: InputMaybe<Scalars['String']['input']>;
-  shipping: Scalars['Money']['input'];
+  /** @deprecated Use the `amount` field instead */
+  shipping?: InputMaybe<Scalars['Money']['input']>;
 };
 
 export type RefundOrderResult = AlreadyRefundedError | MultipleOrderError | NothingToRefundError | OrderStateTransitionError | PaymentOrderMismatchError | QuantityTooGreatError | Refund | RefundAmountError | RefundOrderStateError | RefundStateTransitionError;
@@ -5580,7 +5586,7 @@ export type RemoveFacetsFromChannelInput = {
 
 export type RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError;
 
-export type RemoveOrderItemsResult = Order | OrderModificationError;
+export type RemoveOrderItemsResult = Order | OrderInterceptorError | OrderModificationError;
 
 export type RemovePaymentMethodsFromChannelInput = {
   channelId: Scalars['ID']['input'];
@@ -6096,7 +6102,6 @@ export type StringStructFieldConfig = StructField & {
   length?: Maybe<Scalars['Int']['output']>;
   list: Scalars['Boolean']['output'];
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   options?: Maybe<Array<StringFieldOption>>;
   pattern?: Maybe<Scalars['String']['output']>;
   type: Scalars['String']['output'];
@@ -6123,7 +6128,6 @@ export type StructField = {
   label?: Maybe<Array<LocalizedString>>;
   list?: Maybe<Scalars['Boolean']['output']>;
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -6355,7 +6359,6 @@ export type TextStructFieldConfig = StructField & {
   label?: Maybe<Array<LocalizedString>>;
   list: Scalars['Boolean']['output'];
   name: Scalars['String']['output'];
-  nullable?: Maybe<Scalars['Boolean']['output']>;
   type: Scalars['String']['output'];
   ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -6529,7 +6532,7 @@ export type UpdateOrderInput = {
   id: Scalars['ID']['input'];
 };
 
-export type UpdateOrderItemsResult = InsufficientStockError | NegativeQuantityError | Order | OrderLimitError | OrderModificationError;
+export type UpdateOrderItemsResult = InsufficientStockError | NegativeQuantityError | Order | OrderInterceptorError | OrderLimitError | OrderModificationError;
 
 export type UpdateOrderNoteInput = {
   isPublic?: InputMaybe<Scalars['Boolean']['input']>;

+ 125 - 2
packages/core/e2e/custom-field-struct.e2e-spec.ts

@@ -12,8 +12,6 @@ import { fixPostgresTimezone } from './utils/fix-pg-timezone';
 
 fixPostgresTimezone();
 
-const validateInjectorSpy = vi.fn();
-
 const customConfig = mergeConfig(testConfig(), {
     dbConnectionOptions: {
         timezone: 'Z',
@@ -67,6 +65,26 @@ const customConfig = mergeConfig(testConfig(), {
                 ],
             },
         ],
+        OrderLine: [
+            {
+                type: 'struct',
+                name: 'fromBundle',
+                fields: [
+                    { name: 'bundleId', type: 'string' },
+                    { name: 'bundleName', type: 'string' },
+                ],
+            },
+        ],
+        Address: [
+            {
+                name: 'geoLocation',
+                type: 'struct',
+                fields: [
+                    { name: 'latitude', type: 'float' },
+                    { name: 'longitude', type: 'float' },
+                ],
+            },
+        ],
     },
 });
 
@@ -289,6 +307,111 @@ describe('Custom field struct type', () => {
         });
     });
 
+    it('updating OrderLine custom fields', async () => {
+        const result = await shopClient.query(gql`
+            mutation {
+                addItemToOrder(
+                    productVariantId: "T_1"
+                    quantity: 1
+                    customFields: { fromBundle: { bundleId: "bundle-1", bundleName: "Bundle 1" } }
+                ) {
+                    ... on Order {
+                        id
+                        lines {
+                            id
+                            customFields {
+                                fromBundle {
+                                    bundleId
+                                    bundleName
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        `);
+
+        expect(result.addItemToOrder.lines[0].customFields).toEqual({
+            fromBundle: {
+                bundleId: 'bundle-1',
+                bundleName: 'Bundle 1',
+            },
+        });
+    });
+
+    it('updating Address custom fields', async () => {
+        const result = await adminClient.query(gql`
+            mutation {
+                updateCustomerAddress(
+                    input: { id: "T_1", customFields: { geoLocation: { latitude: 1.23, longitude: 4.56 } } }
+                ) {
+                    id
+                    customFields {
+                        geoLocation {
+                            latitude
+                            longitude
+                        }
+                    }
+                }
+            }
+        `);
+
+        expect(result.updateCustomerAddress.customFields).toEqual({
+            geoLocation: {
+                latitude: 1.23,
+                longitude: 4.56,
+            },
+        });
+    });
+
+    it('updating OrderAddress custom fields', async () => {
+        const result = await shopClient.query(
+            gql`
+                mutation SetShippingAddress($input: CreateAddressInput!) {
+                    setOrderShippingAddress(input: $input) {
+                        ... on Order {
+                            id
+                            shippingAddress {
+                                customFields {
+                                    geoLocation {
+                                        latitude
+                                        longitude
+                                    }
+                                }
+                            }
+                        }
+                        ... on ErrorResult {
+                            errorCode
+                            message
+                        }
+                    }
+                }
+            `,
+            {
+                input: {
+                    fullName: 'name',
+                    streetLine1: '12 the street',
+                    city: 'foo',
+                    postalCode: '123456',
+                    countryCode: 'US',
+                    customFields: {
+                        geoLocation: {
+                            latitude: 1.23,
+                            longitude: 4.56,
+                        },
+                    },
+                },
+            },
+        );
+
+        expect(result.setOrderShippingAddress.shippingAddress.customFields).toEqual({
+            geoLocation: {
+                latitude: 1.23,
+                longitude: 4.56,
+            },
+        });
+    });
+
     describe('struct list', () => {
         it('is initially an empty array', async () => {
             const result = await adminClient.query(gql`

File diff suppressed because it is too large
+ 435 - 457
packages/core/e2e/graphql/generated-e2e-admin-types.ts


+ 62 - 8
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -147,7 +147,6 @@ export type BooleanStructFieldConfig = StructField & {
     label?: Maybe<Array<LocalizedString>>;
     list: Scalars['Boolean']['output'];
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -877,7 +876,6 @@ export type DateTimeStructFieldConfig = StructField & {
     max?: Maybe<Scalars['String']['output']>;
     min?: Maybe<Scalars['String']['output']>;
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     step?: Maybe<Scalars['Int']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
@@ -927,6 +925,7 @@ export enum ErrorCode {
     NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
     NOT_VERIFIED_ERROR = 'NOT_VERIFIED_ERROR',
     NO_ACTIVE_ORDER_ERROR = 'NO_ACTIVE_ORDER_ERROR',
+    ORDER_INTERCEPTOR_ERROR = 'ORDER_INTERCEPTOR_ERROR',
     ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
     ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
     ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
@@ -1115,7 +1114,6 @@ export type FloatStructFieldConfig = StructField & {
     max?: Maybe<Scalars['Float']['output']>;
     min?: Maybe<Scalars['Float']['output']>;
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     step?: Maybe<Scalars['Float']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
@@ -1300,7 +1298,6 @@ export type IntStructFieldConfig = StructField & {
     max?: Maybe<Scalars['Int']['output']>;
     min?: Maybe<Scalars['Int']['output']>;
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     step?: Maybe<Scalars['Int']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
@@ -2059,6 +2056,13 @@ export type OrderFilterParameter = {
     updatedAt?: InputMaybe<DateOperators>;
 };
 
+/** Returned when an order operation is rejected by an OrderInterceptor method. */
+export type OrderInterceptorError = ErrorResult & {
+    errorCode: ErrorCode;
+    interceptorError: Scalars['String']['output'];
+    message: Scalars['String']['output'];
+};
+
 /** Returned when the maximum order size limit has been reached. */
 export type OrderLimitError = ErrorResult & {
     errorCode: ErrorCode;
@@ -2953,7 +2957,7 @@ export type RelationCustomFieldConfig = CustomField & {
     ui?: Maybe<Scalars['JSON']['output']>;
 };
 
-export type RemoveOrderItemsResult = Order | OrderModificationError;
+export type RemoveOrderItemsResult = Order | OrderInterceptorError | OrderModificationError;
 
 export type RequestPasswordResetResult = NativeAuthStrategyError | Success;
 
@@ -3173,7 +3177,6 @@ export type StringStructFieldConfig = StructField & {
     length?: Maybe<Scalars['Int']['output']>;
     list: Scalars['Boolean']['output'];
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     options?: Maybe<Array<StringFieldOption>>;
     pattern?: Maybe<Scalars['String']['output']>;
     type: Scalars['String']['output'];
@@ -3199,7 +3202,6 @@ export type StructField = {
     label?: Maybe<Array<LocalizedString>>;
     list?: Maybe<Scalars['Boolean']['output']>;
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -3291,7 +3293,6 @@ export type TextStructFieldConfig = StructField & {
     label?: Maybe<Array<LocalizedString>>;
     list: Scalars['Boolean']['output'];
     name: Scalars['String']['output'];
-    nullable?: Maybe<Scalars['Boolean']['output']>;
     type: Scalars['String']['output'];
     ui?: Maybe<Scalars['JSON']['output']>;
 };
@@ -3349,6 +3350,7 @@ export type UpdateOrderItemsResult =
     | InsufficientStockError
     | NegativeQuantityError
     | Order
+    | OrderInterceptorError
     | OrderLimitError
     | OrderModificationError;
 
@@ -3561,6 +3563,7 @@ export type AddItemToOrderMutation = {
                   type: AdjustmentType;
               }>;
           }
+        | { errorCode: ErrorCode; message: string; interceptorError: string }
         | { errorCode: ErrorCode; message: string }
         | { errorCode: ErrorCode; message: string };
 };
@@ -3858,6 +3861,7 @@ export type AdjustItemQuantityMutation = {
               customer?: { id: string; user?: { id: string; identifier: string } | null } | null;
               history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
           }
+        | { errorCode: ErrorCode; message: string; interceptorError: string }
         | { errorCode: ErrorCode; message: string }
         | { errorCode: ErrorCode; message: string };
 };
@@ -3915,6 +3919,7 @@ export type RemoveItemFromOrderMutation = {
               customer?: { id: string; user?: { id: string; identifier: string } | null } | null;
               history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
           }
+        | { errorCode: ErrorCode; message: string; interceptorError: string }
         | { errorCode: ErrorCode; message: string };
 };
 
@@ -4875,6 +4880,7 @@ export type RemoveAllOrderLinesMutation = {
               customer?: { id: string; user?: { id: string; identifier: string } | null } | null;
               history: { items: Array<{ id: string; type: HistoryEntryType; data: any }> };
           }
+        | { errorCode: ErrorCode; message: string }
         | { errorCode: ErrorCode; message: string };
 };
 
@@ -5562,6 +5568,22 @@ export const AddItemToOrderDocument = {
                                         ],
                                     },
                                 },
+                                {
+                                    kind: 'InlineFragment',
+                                    typeCondition: {
+                                        kind: 'NamedType',
+                                        name: { kind: 'Name', value: 'OrderInterceptorError' },
+                                    },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'interceptorError' },
+                                            },
+                                        ],
+                                    },
+                                },
                             ],
                         },
                     },
@@ -6964,6 +6986,22 @@ export const AdjustItemQuantityDocument = {
                                         ],
                                     },
                                 },
+                                {
+                                    kind: 'InlineFragment',
+                                    typeCondition: {
+                                        kind: 'NamedType',
+                                        name: { kind: 'Name', value: 'OrderInterceptorError' },
+                                    },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'interceptorError' },
+                                            },
+                                        ],
+                                    },
+                                },
                             ],
                         },
                     },
@@ -7173,6 +7211,22 @@ export const RemoveItemFromOrderDocument = {
                                         ],
                                     },
                                 },
+                                {
+                                    kind: 'InlineFragment',
+                                    typeCondition: {
+                                        kind: 'NamedType',
+                                        name: { kind: 'Name', value: 'OrderInterceptorError' },
+                                    },
+                                    selectionSet: {
+                                        kind: 'SelectionSet',
+                                        selections: [
+                                            {
+                                                kind: 'Field',
+                                                name: { kind: 'Name', value: 'interceptorError' },
+                                            },
+                                        ],
+                                    },
+                                },
                             ],
                         },
                     },

+ 9 - 0
packages/core/e2e/graphql/shop-definitions.ts

@@ -122,6 +122,9 @@ export const ADD_ITEM_TO_ORDER = gql`
                     ...UpdatedOrder
                 }
             }
+            ... on OrderInterceptorError {
+                interceptorError
+            }
         }
     }
     ${UPDATED_ORDER_FRAGMENT}
@@ -372,6 +375,9 @@ export const ADJUST_ITEM_QUANTITY = gql`
                 errorCode
                 message
             }
+            ... on OrderInterceptorError {
+                interceptorError
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}
@@ -385,6 +391,9 @@ export const REMOVE_ITEM_FROM_ORDER = gql`
                 errorCode
                 message
             }
+            ... on OrderInterceptorError {
+                interceptorError
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}

+ 290 - 0
packages/core/e2e/order-interceptor.e2e-spec.ts

@@ -0,0 +1,290 @@
+import {
+    mergeConfig,
+    Order,
+    OrderInterceptor,
+    OrderLine,
+    RequestContext,
+    WillAddItemToOrderInput,
+    WillAdjustOrderLineInput,
+} from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import path from 'path';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import {
+    ADD_ITEM_TO_ORDER,
+    ADJUST_ITEM_QUANTITY,
+    GET_ACTIVE_ORDER,
+    REMOVE_ITEM_FROM_ORDER,
+} from './graphql/shop-definitions';
+
+class Interceptor1 implements OrderInterceptor {
+    willAddItemToOrderSpy = vi.fn();
+    willAdjustOrderLineSpy = vi.fn();
+    willRemoveItemFromOrderSpy = vi.fn();
+
+    willAddItemToOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAddItemToOrderInput,
+    ): Promise<void | string> {
+        this.willAddItemToOrderSpy(ctx, order, input);
+        return Promise.resolve();
+    }
+
+    willAdjustOrderLine(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAdjustOrderLineInput,
+    ): Promise<void | string> {
+        this.willAdjustOrderLineSpy(ctx, order, input);
+        return Promise.resolve();
+    }
+
+    willRemoveItemFromOrder(ctx: RequestContext, order: Order, orderLine: OrderLine): Promise<void | string> {
+        this.willRemoveItemFromOrderSpy(ctx, order, orderLine);
+        return Promise.resolve();
+    }
+}
+
+class Interceptor2 implements OrderInterceptor {
+    async willAddItemToOrder(ctx: RequestContext, order: Order, input: WillAddItemToOrderInput) {
+        if (input.productVariant.id === 2 && input.quantity < 2) {
+            return 'Quantity must be at least 2';
+        }
+    }
+
+    async willAdjustOrderLine(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAdjustOrderLineInput,
+    ): Promise<void | string> {
+        if (input.orderLine.productVariant.id === 2 && input.quantity < 2) {
+            return 'Quantity must be at least 2';
+        }
+    }
+
+    async willRemoveItemFromOrder(
+        ctx: RequestContext,
+        order: Order,
+        orderLine: OrderLine,
+    ): Promise<void | string> {
+        const overridden = ctx.req?.query?.overridden;
+        if (overridden) {
+            return;
+        }
+        if (orderLine.productVariant.id === 2) {
+            return 'Cannot remove this item';
+        }
+    }
+}
+
+type OrderSuccessResult =
+    | CodegenShop.UpdatedOrderFragment
+    | CodegenShop.TestOrderFragmentFragment
+    | CodegenShop.TestOrderWithPaymentsFragment
+    | CodegenShop.ActiveOrderCustomerFragment
+    | CodegenShop.OrderWithAddressesFragment;
+const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(input => !!input.lines);
+
+describe('Order interceptor', () => {
+    const interceptor1 = new Interceptor1();
+    const interceptor2 = new Interceptor2();
+
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            orderOptions: {
+                orderInterceptors: [interceptor1, interceptor2],
+            },
+        }),
+    );
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('willAddItemToOrder', async () => {
+        const { addItemToOrder } = await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_1',
+            quantity: 1,
+        });
+
+        orderResultGuard.assertSuccess(addItemToOrder);
+        expect(addItemToOrder.lines.length).toBe(1);
+
+        expect(interceptor1.willAddItemToOrderSpy).toHaveBeenCalled();
+        expect(interceptor1.willAddItemToOrderSpy.mock.calls[0][0]).toBeInstanceOf(RequestContext);
+        expect(interceptor1.willAddItemToOrderSpy.mock.calls[0][1]).toBeInstanceOf(Order);
+        expect(interceptor1.willAddItemToOrderSpy.mock.calls[0][2].quantity).toBe(1);
+        expect(interceptor1.willAddItemToOrderSpy.mock.calls[0][2].productVariant.id).toBe(1);
+        expect(interceptor1.willAddItemToOrderSpy.mock.calls[0][2].customFields).toBeUndefined();
+    });
+
+    it('willAdjustOrderLine', async () => {
+        const { adjustOrderLine } = await shopClient.query<
+            CodegenShop.AdjustItemQuantityMutation,
+            CodegenShop.AdjustItemQuantityMutationVariables
+        >(ADJUST_ITEM_QUANTITY, {
+            orderLineId: 'T_1',
+            quantity: 2,
+        });
+
+        orderResultGuard.assertSuccess(adjustOrderLine);
+
+        expect(interceptor1.willAdjustOrderLineSpy).toHaveBeenCalled();
+        expect(interceptor1.willAdjustOrderLineSpy.mock.calls[0][0]).toBeInstanceOf(RequestContext);
+        expect(interceptor1.willAdjustOrderLineSpy.mock.calls[0][1]).toBeInstanceOf(Order);
+        expect(interceptor1.willAdjustOrderLineSpy.mock.calls[0][2].quantity).toBe(2);
+        expect(interceptor1.willAdjustOrderLineSpy.mock.calls[0][2].orderLine.id).toBe(1);
+        expect(interceptor1.willAdjustOrderLineSpy.mock.calls[0][2].customFields).toBeUndefined();
+    });
+
+    it('willRemoveItemFromOrder', async () => {
+        const { removeOrderLine } = await shopClient.query<
+            CodegenShop.RemoveItemFromOrderMutation,
+            CodegenShop.RemoveItemFromOrderMutationVariables
+        >(REMOVE_ITEM_FROM_ORDER, {
+            orderLineId: 'T_1',
+        });
+
+        orderResultGuard.assertSuccess(removeOrderLine);
+
+        expect(interceptor1.willRemoveItemFromOrderSpy).toHaveBeenCalled();
+        expect(interceptor1.willRemoveItemFromOrderSpy.mock.calls[0][0]).toBeInstanceOf(RequestContext);
+        expect(interceptor1.willRemoveItemFromOrderSpy.mock.calls[0][1]).toBeInstanceOf(Order);
+        expect(interceptor1.willRemoveItemFromOrderSpy.mock.calls[0][2].productVariant.id).toEqual(1);
+    });
+
+    it('willAddItemToOrder with error', async () => {
+        const { addItemToOrder } = await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_2',
+            quantity: 1,
+        });
+
+        orderResultGuard.assertErrorResult(addItemToOrder);
+        expect(addItemToOrder.message).toBe('An error occurred when attempting to modify the Order');
+        expect(addItemToOrder.interceptorError).toBe('Quantity must be at least 2');
+    });
+
+    it('item was not added to order', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        orderResultGuard.assertSuccess(activeOrder);
+        expect(activeOrder.lines.length).toBe(0);
+    });
+
+    it('add item that passes interceptor check', async () => {
+        const { addItemToOrder } = await shopClient.query<
+            CodegenShop.AddItemToOrderMutation,
+            CodegenShop.AddItemToOrderMutationVariables
+        >(ADD_ITEM_TO_ORDER, {
+            productVariantId: 'T_2',
+            quantity: 2,
+        });
+
+        orderResultGuard.assertSuccess(addItemToOrder);
+        expect(addItemToOrder.lines.length).toBe(1);
+    });
+
+    it('willAdjustOrderLine with error', async () => {
+        const { adjustOrderLine } = await shopClient.query<
+            CodegenShop.AdjustItemQuantityMutation,
+            CodegenShop.AdjustItemQuantityMutationVariables
+        >(ADJUST_ITEM_QUANTITY, {
+            orderLineId: 'T_2',
+            quantity: 1,
+        });
+
+        orderResultGuard.assertErrorResult(adjustOrderLine);
+
+        expect(adjustOrderLine.message).toBe('An error occurred when attempting to modify the Order');
+        expect(adjustOrderLine.interceptorError).toBe('Quantity must be at least 2');
+    });
+
+    it('item was not adjusted', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        orderResultGuard.assertSuccess(activeOrder);
+        expect(activeOrder.lines.length).toBe(1);
+        expect(activeOrder.lines[0].quantity).toBe(2);
+    });
+
+    it('adjust item that passes interceptor check', async () => {
+        const { adjustOrderLine } = await shopClient.query<
+            CodegenShop.AdjustItemQuantityMutation,
+            CodegenShop.AdjustItemQuantityMutationVariables
+        >(ADJUST_ITEM_QUANTITY, {
+            orderLineId: 'T_2',
+            quantity: 5,
+        });
+
+        orderResultGuard.assertSuccess(adjustOrderLine);
+        expect(adjustOrderLine.lines.length).toBe(1);
+        expect(adjustOrderLine.lines[0].quantity).toBe(5);
+    });
+
+    it('willRemoveItemFromOrder with error', async () => {
+        const { removeOrderLine } = await shopClient.query<
+            CodegenShop.RemoveItemFromOrderMutation,
+            CodegenShop.RemoveItemFromOrderMutationVariables
+        >(REMOVE_ITEM_FROM_ORDER, {
+            orderLineId: 'T_2',
+        });
+
+        orderResultGuard.assertErrorResult(removeOrderLine);
+
+        expect(removeOrderLine.message).toBe('An error occurred when attempting to modify the Order');
+        expect(removeOrderLine.interceptorError).toBe('Cannot remove this item');
+    });
+
+    it('item was not removed', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        orderResultGuard.assertSuccess(activeOrder);
+        expect(activeOrder.lines.length).toBe(1);
+        expect(activeOrder.lines[0].quantity).toBe(5);
+    });
+
+    it('remove item that passes interceptor check', async () => {
+        const { removeOrderLine } = await shopClient.query<
+            CodegenShop.RemoveItemFromOrderMutation,
+            CodegenShop.RemoveItemFromOrderMutationVariables
+        >(
+            REMOVE_ITEM_FROM_ORDER,
+            {
+                orderLineId: 'T_2',
+            },
+            {
+                overridden: 1,
+            },
+        );
+
+        orderResultGuard.assertSuccess(removeOrderLine);
+        expect(removeOrderLine.lines.length).toBe(0);
+    });
+
+    it('item was removed', async () => {
+        const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+
+        orderResultGuard.assertSuccess(activeOrder);
+        expect(activeOrder.lines.length).toBe(0);
+    });
+});

+ 1 - 1
packages/core/src/api/common/validate-custom-field-value.ts

@@ -143,7 +143,7 @@ function validateStringField(
     const options = (config as StringCustomFieldConfig).options;
     if (options) {
         const validOptions = options.map(o => o.value);
-        if (value === null && config.nullable === true) {
+        if (value === null && (config as StringCustomFieldConfig).nullable === true) {
             return;
         }
         if (!validOptions.includes(value)) {

+ 30 - 3
packages/core/src/api/config/graphql-custom-fields.ts

@@ -404,17 +404,44 @@ export function addOrderLineCustomFieldsInput(
     if (!mutationType) {
         return schema;
     }
+    const structFields = orderLineCustomFields.filter(
+        (f): f is StructCustomFieldConfig => f.type === 'struct',
+    );
+    const structInputTypes: GraphQLInputObjectType[] = [];
+    if (0 < structFields.length) {
+        for (const structField of structFields) {
+            const structInputName = getStructInputName('OrderLine', structField);
+            structInputTypes.push(
+                new GraphQLInputObjectType({
+                    name: structInputName,
+                    fields: structField.fields.reduce((fields, field) => {
+                        const name = getGraphQlInputName(field);
+                        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                        const primitiveType = schema.getType(getGraphQlInputType('OrderLine')(field))!;
+                        const type = field.list === true ? new GraphQLList(primitiveType) : primitiveType;
+                        return { ...fields, [name]: { type } };
+                    }, {}),
+                }),
+            );
+        }
+    }
     const input = new GraphQLInputObjectType({
         name: 'OrderLineCustomFieldsInput',
         fields: publicCustomFields.reduce((fields, field) => {
             const name = getGraphQlInputName(field);
+            const inputTypeName = getGraphQlInputType('OrderLine')(field);
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            const primitiveType = schema.getType(getGraphQlInputType('OrderLine')(field))!;
-            const type = field.list === true ? new GraphQLList(primitiveType) : primitiveType;
+            const inputType =
+                schema.getType(getGraphQlInputType('OrderLine')(field)) ??
+                structInputTypes.find(t => t.name === inputTypeName);
+            if (!inputType) {
+                throw new Error(`Could not find input type for field ${field.name}`);
+            }
+            const type = field.list === true ? new GraphQLList(inputType) : inputType;
             return { ...fields, [name]: { type } };
         }, {}),
     });
-    schemaConfig.types = [...schemaConfig.types, input];
+    schemaConfig.types = [...schemaConfig.types, ...structInputTypes, input];
 
     const addItemToOrderMutation = mutationType.getFields().addItemToOrder;
     const adjustOrderLineMutation = mutationType.getFields().adjustOrderLine;

+ 9 - 0
packages/core/src/api/schema/common/common-error-results.graphql

@@ -96,3 +96,12 @@ type NoActiveOrderError implements ErrorResult {
     errorCode: ErrorCode!
     message: String!
 }
+
+"""
+Returned when an order operation is rejected by an OrderInterceptor method.
+"""
+type OrderInterceptorError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    interceptorError: String!
+}

+ 2 - 1
packages/core/src/api/schema/common/common-types.graphql

@@ -274,7 +274,8 @@ union UpdateOrderItemsResult =
     | OrderLimitError
     | NegativeQuantityError
     | InsufficientStockError
-union RemoveOrderItemsResult = Order | OrderModificationError
+    | OrderInterceptorError
+union RemoveOrderItemsResult = Order | OrderModificationError | OrderInterceptorError
 union SetOrderShippingMethodResult =
       Order
     | OrderModificationError

+ 14 - 1
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -403,6 +403,19 @@ export class NothingToRefundError extends ErrorResult {
   }
 }
 
+export class OrderInterceptorError extends ErrorResult {
+  readonly __typename = 'OrderInterceptorError';
+  readonly errorCode = 'ORDER_INTERCEPTOR_ERROR' as any;
+  readonly message = 'ORDER_INTERCEPTOR_ERROR';
+  readonly interceptorError: Scalars['String'];
+  constructor(
+    input: { interceptorError: Scalars['String'] }
+  ) {
+    super();
+    this.interceptorError = input.interceptorError
+  }
+}
+
 export class OrderLimitError extends ErrorResult {
   readonly __typename = 'OrderLimitError';
   readonly errorCode = 'ORDER_LIMIT_ERROR' as any;
@@ -588,7 +601,7 @@ export class SettlePaymentError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'DuplicateEntityError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'GuestCheckoutError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundAmountError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
+const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'DuplicateEntityError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'GuestCheckoutError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderInterceptorError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundAmountError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 14 - 1
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -224,6 +224,19 @@ export class NotVerifiedError extends ErrorResult {
   }
 }
 
+export class OrderInterceptorError extends ErrorResult {
+  readonly __typename = 'OrderInterceptorError';
+  readonly errorCode = 'ORDER_INTERCEPTOR_ERROR' as any;
+  readonly message = 'ORDER_INTERCEPTOR_ERROR';
+  readonly interceptorError: Scalars['String'];
+  constructor(
+    input: { interceptorError: Scalars['String'] }
+  ) {
+    super();
+    this.interceptorError = input.interceptorError
+  }
+}
+
 export class OrderLimitError extends ErrorResult {
   readonly __typename = 'OrderLimitError';
   readonly errorCode = 'ORDER_LIMIT_ERROR' as any;
@@ -371,7 +384,7 @@ export class VerificationTokenInvalidError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set<string>(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'GuestCheckoutError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordValidationError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
+const errorTypeNames = new Set<string>(['AlreadyLoggedInError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'EmailAddressConflictError', 'GuestCheckoutError', 'IdentifierChangeTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IneligiblePaymentMethodError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InvalidCredentialsError', 'MissingPasswordError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NotVerifiedError', 'OrderInterceptorError', 'OrderLimitError', 'OrderModificationError', 'OrderPaymentStateError', 'OrderStateTransitionError', 'PasswordAlreadySetError', 'PasswordResetTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordValidationError', 'PaymentDeclinedError', 'PaymentFailedError', 'VerificationTokenExpiredError', 'VerificationTokenInvalidError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -98,6 +98,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             changedPriceHandlingStrategy,
             orderSellerStrategy,
             guestCheckoutStrategy,
+            orderInterceptors,
         } = this.configService.orderOptions;
         const {
             customFulfillmentProcess,
@@ -152,6 +153,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             guestCheckoutStrategy,
             ...refundProcess,
             cacheStrategy,
+            ...orderInterceptors,
         ];
     }
 

+ 1 - 0
packages/core/src/config/default-config.ts

@@ -168,6 +168,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         activeOrderStrategy: new DefaultActiveOrderStrategy(),
         orderSellerStrategy: new DefaultOrderSellerStrategy(),
         guestCheckoutStrategy: new DefaultGuestCheckoutStrategy(),
+        orderInterceptors: [],
     },
     paymentOptions: {
         paymentMethodEligibilityCheckers: [],

+ 1 - 0
packages/core/src/config/index.ts

@@ -57,6 +57,7 @@ export * from './order/guest-checkout-strategy';
 export * from './order/merge-orders-strategy';
 export * from './order/order-by-code-access-strategy';
 export * from './order/order-code-strategy';
+export * from './order/order-interceptor';
 export * from './order/order-item-price-calculation-strategy';
 export * from './order/order-merge-strategy';
 export * from './order/order-placed-strategy';

+ 250 - 0
packages/core/src/config/order/order-interceptor.ts

@@ -0,0 +1,250 @@
+import { RequestContext } from '../../api/common/request-context';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { CustomOrderLineFields, Order, OrderLine, ProductVariant } from '../../entity/index';
+
+export interface WillAddItemToOrderInput {
+    productVariant: ProductVariant;
+    quantity: number;
+    customFields?: CustomOrderLineFields;
+}
+
+export interface WillAdjustOrderLineInput {
+    orderLine: OrderLine;
+    quantity: number;
+    customFields?: CustomOrderLineFields;
+}
+
+/**
+ * @description
+ * An OrderInterceptor is a class which can be used to intercept and modify the behavior of order-related
+ * operations.
+ *
+ * It does this by providing methods which are called whenever the contents of an order are about
+ * to get changed. These methods are able to prevent the operation from proceeding by returning a string
+ * error message.
+ *
+ * Examples of use-cases for an OrderInterceptor include:
+ *
+ * * Preventing certain products from being added to the order based on some criteria, e.g. if the
+ *   product is already in another active order.
+ * * Enforcing a minimum or maximum quantity of a given product in the order
+ * * Using a CAPTCHA to prevent automated order creation
+ *
+ * :::info
+ *
+ * This is configured via the `orderOptions.orderInterceptors` property of
+ * your VendureConfig.
+ *
+ * :::
+ *
+ * OrderInterceptors are executed when the following mutations are called:
+ *
+ * - `addItemToOrder`
+ * - `adjustOrderLine`
+ * - `removeItemFromOrder`
+ *
+ * Additionally, if you are working directly with the {@link OrderService}, the following methods will trigger
+ * any registered OrderInterceptors:
+ *
+ * - `addItemToOrder`
+ * - `addItemsToOrder`
+ * - `adjustOrderLine`
+ * - `adjustOrderLines`
+ * - `removeItemFromOrder`
+ * - `removeItemsFromOrder`
+ *
+ * When an OrderInterceptor is registered, it will be called in the order in which it was registered.
+ * If an interceptor method resolves to a string, the operation will be prevented and the string will be used as the error message.
+ *
+ * When multiple interceptors are registered, the first interceptor to resolve to a string will prevent the operation from proceeding.
+ *
+ * Errors returned by OrderInterceptors are surfaced to the GraphQL API as an `OrderInterceptorError` and can be
+ * queried like this:
+ *
+ * ```graphql
+ * mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
+ *   addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
+ *     ... on Order {
+ *       id
+ *       code
+ *       # ... other Order fields
+ *     }
+ *     ... on ErrorResult {
+ *       errorCode
+ *       message
+ *     }
+ *     // highlight-start
+ *     ... on OrderInterceptorError {
+ *       interceptorError
+ *     }
+ *     // highlight-end
+ *   }
+ * }
+ * ```
+ *
+ * In the above example, the error message returned by the OrderInterceptor would be available in the `interceptorError` field.
+ *
+ * ## Example: Min/max order quantity
+ *
+ * Let's say we want to allow ProductVariants to specify the minimum or maximum amount which may be added
+ * to an order. We can define custom fields to store this information and
+ * then use this custom field value to prevent an order line from being added to the order if the quantity
+ * is below the minimum.
+ *
+ * @example
+ * ```ts
+ * import {
+ *   EntityHydrator,
+ *   Injector,
+ *   LanguageCode,
+ *   Order,
+ *   OrderInterceptor,
+ *   ProductVariant,
+ *   RequestContext,
+ *   TranslatorService,
+ *   VendurePlugin,
+ *   WillAddItemToOrderInput,
+ *   WillAdjustOrderLineInput,
+ * } from '\@vendure/core';
+ *
+ * declare module '\@vendure/core/dist/entity/custom-entity-fields' {
+ *   interface CustomProductVariantFields {
+ *     minOrderQuantity?: number;
+ *     maxOrderQuantity?: number;
+ *   }
+ * }
+ *
+ * // This OrderInterceptor enforces minimum and maximum order quantities on ProductVariants.
+ * export class MinMaxOrderInterceptor implements OrderInterceptor {
+ *  private entityHydrator: EntityHydrator;
+ *  private translatorService: TranslatorService;
+ *
+ *  init(injector: Injector) {
+ *    this.entityHydrator = injector.get(EntityHydrator);
+ *    this.translatorService = injector.get(TranslatorService);
+ *  }
+ *
+ *  willAddItemToOrder(
+ *    ctx: RequestContext,
+ *    order: Order,
+ *    input: WillAddItemToOrderInput,
+ *  ): Promise<void | string> | void | string {
+ *    const { productVariant, quantity } = input;
+ *    const min = productVariant.customFields?.minOrderQuantity;
+ *    const max = productVariant.customFields?.maxOrderQuantity;
+ *    if (min && quantity < min) {
+ *      return this.minErrorMessage(ctx, productVariant, min);
+ *    }
+ *    if (max && quantity > max) {
+ *      return this.maxErrorMessage(ctx, productVariant, max);
+ *    }
+ *  }
+ *
+ *  willAdjustOrderLine(
+ *    ctx: RequestContext,
+ *    order: Order,
+ *    input: WillAdjustOrderLineInput,
+ *  ): Promise<void | string> | void | string {
+ *    const { orderLine, quantity } = input;
+ *    const min = orderLine.productVariant.customFields?.minOrderQuantity;
+ *    const max = orderLine.productVariant.customFields?.maxOrderQuantity;
+ *    if (min && quantity < min) {
+ *      return this.minErrorMessage(ctx, orderLine.productVariant, min);
+ *    }
+ *    if (max && quantity > max) {
+ *      return this.maxErrorMessage(ctx, orderLine.productVariant, max);
+ *    }
+ *  }
+ *
+ *  private async minErrorMessage(ctx: RequestContext, variant: ProductVariant, min: number) {
+ *    const variantName = await this.getTranslatedVariantName(ctx, variant);
+ *    return `Minimum order quantity for "${variantName}" is ${min}`;
+ *  }
+ *
+ *  private async maxErrorMessage(ctx: RequestContext, variant: ProductVariant, max: number) {
+ *    const variantName = await this.getTranslatedVariantName(ctx, variant);
+ *    return `Maximum order quantity for "${variantName}" is ${max}`;
+ *  }
+ *
+ *  private async getTranslatedVariantName(ctx: RequestContext, variant: ProductVariant) {
+ *    await this.entityHydrator.hydrate(ctx, variant, { relations: ['translations'] });
+ *    const translated = this.translatorService.translate(variant, ctx);
+ *    return translated.name;
+ *  }
+ * }
+ *
+ * // This plugin enforces minimum and maximum order quantities on ProductVariants.
+ * // It adds two new custom fields to ProductVariant:
+ * // - minOrderQuantity
+ * // - maxOrderQuantity
+ * //
+ * // It also adds an OrderInterceptor which enforces these limits.
+ * \@VendurePlugin({
+ *  configuration: config => {
+ *  // Here we add the custom fields to the ProductVariant entity
+ *    config.customFields.ProductVariant.push({
+ *      type: 'int',
+ *      min: 0,
+ *      name: 'minOrderQuantity',
+ *      label: [{ languageCode: LanguageCode.en, value: 'Minimum order quantity' }],
+ *      nullable: true,
+ *    });
+ *    config.customFields.ProductVariant.push({
+ *      type: 'int',
+ *      min: 0,
+ *      name: 'maxOrderQuantity',
+ *      label: [{ languageCode: LanguageCode.en, value: 'Maximum order quantity' }],
+ *      nullable: true,
+ *    });
+ *
+ *    // Here we add the MinMaxOrderInterceptor to the orderInterceptors array
+ *    config.orderOptions.orderInterceptors.push(new MinMaxOrderInterceptor());
+ *    return config;
+ *  },
+ * })
+ * export class OrderQuantityLimitsPlugin {}
+ * ```
+ *
+ *
+ * @docsCategory orders
+ * @docsPage OrderInterceptor
+ * @docsWeight 0
+ * @since 3.1.0
+ */
+export interface OrderInterceptor extends InjectableStrategy {
+    /**
+     * @description
+     * Called when a new item is about to be added to the order,
+     * as in the `addItemToOrder` mutation or the `addItemToOrder()` / `addItemsToOrder()` method
+     * of the {@link OrderService}.
+     */
+    willAddItemToOrder?(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAddItemToOrderInput,
+    ): Promise<void | string> | void | string;
+
+    /**
+     * @description
+     * Called when an existing order line is about to be adjusted,
+     * as in the `adjustOrderLine` mutation or the `adjustOrderLine()` / `adjustOrderLines()` method
+     * of the {@link OrderService}.
+     */
+    willAdjustOrderLine?(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAdjustOrderLineInput,
+    ): Promise<void | string> | void | string;
+
+    /**
+     * @description
+     * Called when an item is about to be removed from the order,
+     * as in the `removeItemFromOrder` mutation or the `removeItemFromOrder()` / `removeItemsFromOrder()` method
+     * of the {@link OrderService}.
+     */
+    willRemoveItemFromOrder?(
+        ctx: RequestContext,
+        order: Order,
+        orderLine: OrderLine,
+    ): Promise<void | string> | void | string;
+}

+ 11 - 1
packages/core/src/config/vendure-config.ts

@@ -37,6 +37,7 @@ import { ChangedPriceHandlingStrategy } from './order/changed-price-handling-str
 import { GuestCheckoutStrategy } from './order/guest-checkout-strategy';
 import { OrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { OrderCodeStrategy } from './order/order-code-strategy';
+import { OrderInterceptor } from './order/order-interceptor';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
 import { OrderPlacedStrategy } from './order/order-placed-strategy';
@@ -612,10 +613,19 @@ export interface OrderOptions {
      * @description
      * Defines how we deal with guest checkouts.
      *
-     * @since 2.0.0
+     * @sinc
+     * e 2.0.0
      * @default DefaultGuestCheckoutStrategy
      */
     guestCheckoutStrategy?: GuestCheckoutStrategy;
+    /**
+     * @description
+     * An array of {@link OrderInterceptor}s which can be used to modify the behavior of the Order process.
+     *
+     * @since 3.1.0
+     * @default []
+     */
+    orderInterceptors?: OrderInterceptor[];
 }
 
 /**

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -84,6 +84,7 @@
     "NO_ACTIVE_ORDER_ERROR": "There is no active Order associated with the current session",
     "NOTHING_TO_REFUND_ERROR": "Nothing to refund",
     "NOT_VERIFIED_ERROR": "Please verify this email address before logging in",
+    "ORDER_INTERCEPTOR_ERROR": "An error occurred when attempting to modify the Order",
     "ORDER_LIMIT_ERROR": "Cannot add items. An order may consist of a maximum of { maxItems } items",
     "ORDER_MODIFICATION_ERROR": "Order contents may only be modified when in the \"AddingItems\" state",
     "ORDER_PAYMENT_STATE_ERROR": "A Payment may only be added when Order is in \"ArrangingPayment\" state",

+ 12 - 6
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -491,7 +491,7 @@ export class OrderModifier {
                     const qtyDelta = initialLineQuantity - correctedQuantity;
 
                     refundInputs.forEach(ri => {
-                        ri.lines.push({
+                        ri.lines?.push({
                             orderLineId: orderLine.id,
                             quantity: qtyDelta,
                         });
@@ -524,7 +524,11 @@ export class OrderModifier {
             order.surcharges.push(surcharge);
             modification.surcharges.push(surcharge);
             if (surcharge.priceWithTax < 0) {
-                refundInputs.forEach(ri => (ri.adjustment += Math.abs(surcharge.priceWithTax)));
+                refundInputs.forEach(ri => {
+                    if (ri.adjustment != null) {
+                        ri.adjustment += Math.abs(surcharge.priceWithTax);
+                    }
+                });
             }
         }
         if (input.surcharges?.length) {
@@ -659,7 +663,9 @@ export class OrderModifier {
             if (shippingDelta < 0) {
                 primaryRefund.shipping = shippingDelta * -1;
             }
-            primaryRefund.adjustment += await this.calculateRefundAdjustment(ctx, delta, primaryRefund);
+            if (primaryRefund.adjustment != null) {
+                primaryRefund.adjustment += await this.calculateRefundAdjustment(ctx, delta, primaryRefund);
+            }
             // end
 
             for (const refundInput of refundInputs) {
@@ -777,14 +783,14 @@ export class OrderModifier {
         delta: number,
         refundInput: RefundOrderInput,
     ): Promise<number> {
-        const existingAdjustment = refundInput.adjustment;
+        const existingAdjustment = refundInput.adjustment ?? 0;
 
         let itemAmount = 0; // TODO: figure out what this should be
-        for (const lineInput of refundInput.lines) {
+        for (const lineInput of refundInput.lines ?? []) {
             const orderLine = await this.connection.getEntityOrThrow(ctx, OrderLine, lineInput.orderLineId);
             itemAmount += orderLine.proratedUnitPriceWithTax * lineInput.quantity;
         }
-        const calculatedDelta = itemAmount + refundInput.shipping + existingAdjustment;
+        const calculatedDelta = itemAmount + (refundInput.shipping ?? 0) + existingAdjustment;
         const absDelta = Math.abs(delta);
         return absDelta !== calculatedDelta ? absDelta - calculatedDelta : 0;
     }

+ 40 - 2
packages/core/src/service/services/order.service.ts

@@ -66,6 +66,7 @@ import {
 import {
     InsufficientStockError,
     NegativeQuantityError,
+    OrderInterceptorError,
     OrderLimitError,
     OrderModificationError,
     OrderPaymentStateError,
@@ -581,7 +582,7 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> = [];
         const updatedOrderLines: OrderLine[] = [];
-        for (const item of items) {
+        addItem: for (const item of items) {
             const { productVariantId, quantity, customFields } = item;
             const existingOrderLine = await this.orderModifier.getExistingOrderLine(
                 ctx,
@@ -630,6 +631,20 @@ export class OrderService {
                 );
                 continue;
             }
+            const { orderInterceptors } = this.configService.orderOptions;
+            for (const interceptor of orderInterceptors) {
+                if (interceptor.willAddItemToOrder) {
+                    const error = await interceptor.willAddItemToOrder(ctx, order, {
+                        productVariant: variant,
+                        quantity: correctedQuantity,
+                        customFields,
+                    });
+                    if (error) {
+                        errorResults.push(new OrderInterceptorError({ interceptorError: error }));
+                        continue addItem;
+                    }
+                }
+            }
             const orderLine = await this.orderModifier.getOrCreateOrderLine(
                 ctx,
                 order,
@@ -714,7 +729,7 @@ export class OrderService {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const errorResults: Array<JustErrorResults<UpdateOrderItemsResult>> = [];
         const updatedOrderLines: OrderLine[] = [];
-        for (const line of lines) {
+        adjustLine: for (const line of lines) {
             const { orderLineId, quantity, customFields } = line;
             const orderLine = this.getOrderLineOrThrow(order, orderLineId);
             const validationError =
@@ -726,6 +741,20 @@ export class OrderService {
                 errorResults.push(validationError);
                 continue;
             }
+            const { orderInterceptors } = this.configService.orderOptions;
+            for (const interceptor of orderInterceptors) {
+                if (interceptor.willAdjustOrderLine) {
+                    const error = await interceptor.willAdjustOrderLine(ctx, order, {
+                        orderLine,
+                        quantity,
+                        customFields,
+                    });
+                    if (error) {
+                        errorResults.push(new OrderInterceptorError({ interceptorError: error }));
+                        continue adjustLine;
+                    }
+                }
+            }
             if (customFields != null) {
                 orderLine.customFields = customFields;
                 await this.customFieldRelationService.updateRelations(
@@ -820,6 +849,15 @@ export class OrderService {
         const orderLinesToDelete: OrderLine[] = [];
         for (const orderLineId of orderLineIds) {
             const orderLine = this.getOrderLineOrThrow(order, orderLineId);
+            const { orderInterceptors } = this.configService.orderOptions;
+            for (const interceptor of orderInterceptors) {
+                if (interceptor.willRemoveItemFromOrder) {
+                    const error = await interceptor.willRemoveItemFromOrder(ctx, order, orderLine);
+                    if (error) {
+                        return new OrderInterceptorError({ interceptorError: error });
+                    }
+                }
+            }
             orderLinesToDelete.push(orderLine);
         }
 

+ 5 - 4
packages/core/src/service/services/payment.service.ts

@@ -370,7 +370,7 @@ export class PaymentService {
             }
             refund = await this.connection.getRepository(ctx, Refund).save(refund);
             const refundLines: RefundLine[] = [];
-            for (const { orderLineId, quantity } of input.lines) {
+            for (const { orderLineId, quantity } of input.lines || []) {
                 const refundLine = await this.connection.getRepository(ctx, RefundLine).save(
                     new RefundLine({
                         refund,
@@ -447,16 +447,17 @@ export class PaymentService {
         // are involved.
         // It is deprecated and will be removed in a future version.
         let refundOrderLinesTotal = 0;
+        const inputLines = input.lines || [];
         const orderLines = await this.connection
             .getRepository(ctx, OrderLine)
-            .find({ where: { id: In(input.lines.map(l => l.orderLineId)) } });
-        for (const line of input.lines) {
+            .find({ where: { id: In(inputLines.map(l => l.orderLineId)) } });
+        for (const line of inputLines) {
             const orderLine = orderLines.find(l => idsAreEqual(l.id, line.orderLineId));
             if (orderLine && 0 < orderLine.orderPlacedQuantity) {
                 refundOrderLinesTotal += line.quantity * orderLine.proratedUnitPriceWithTax;
             }
         }
-        const total = refundOrderLinesTotal + input.shipping + input.adjustment;
+        const total = refundOrderLinesTotal + (input.shipping ?? 0) + (input.adjustment ?? 0);
         return { orderLinesTotal: refundOrderLinesTotal, total };
     }
 

+ 111 - 0
packages/dev-server/example-plugins/minimum-order-quantity/order-quantity-limits.plugin.ts

@@ -0,0 +1,111 @@
+import {
+    EntityHydrator,
+    Injector,
+    LanguageCode,
+    Order,
+    OrderInterceptor,
+    ProductVariant,
+    RequestContext,
+    TranslatorService,
+    VendurePlugin,
+    WillAddItemToOrderInput,
+    WillAdjustOrderLineInput,
+} from '@vendure/core';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomProductVariantFields {
+        minOrderQuantity?: number;
+        maxOrderQuantity?: number;
+    }
+}
+
+/**
+ * This OrderInterceptor enforces minimum and maximum order quantities on ProductVariants.
+ */
+export class MinMaxOrderInterceptor implements OrderInterceptor {
+    private entityHydrator: EntityHydrator;
+    private translatorService: TranslatorService;
+
+    init(injector: Injector) {
+        this.entityHydrator = injector.get(EntityHydrator);
+        this.translatorService = injector.get(TranslatorService);
+    }
+
+    willAddItemToOrder(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAddItemToOrderInput,
+    ): Promise<void | string> | void | string {
+        const { productVariant, quantity } = input;
+        const min = productVariant.customFields?.minOrderQuantity;
+        const max = productVariant.customFields?.maxOrderQuantity;
+        if (min && quantity < min) {
+            return this.minErrorMessage(ctx, productVariant, min);
+        }
+        if (max && quantity > max) {
+            return this.maxErrorMessage(ctx, productVariant, max);
+        }
+    }
+
+    willAdjustOrderLine(
+        ctx: RequestContext,
+        order: Order,
+        input: WillAdjustOrderLineInput,
+    ): Promise<void | string> | void | string {
+        const { orderLine, quantity } = input;
+        const min = orderLine.productVariant.customFields?.minOrderQuantity;
+        const max = orderLine.productVariant.customFields?.maxOrderQuantity;
+        if (min && quantity < min) {
+            return this.minErrorMessage(ctx, orderLine.productVariant, min);
+        }
+        if (max && quantity > max) {
+            return this.maxErrorMessage(ctx, orderLine.productVariant, max);
+        }
+    }
+
+    private async minErrorMessage(ctx: RequestContext, variant: ProductVariant, min: number) {
+        const variantName = await this.getTranslatedVariantName(ctx, variant);
+        return `Minimum order quantity for "${variantName}" is ${min}`;
+    }
+
+    private async maxErrorMessage(ctx: RequestContext, variant: ProductVariant, max: number) {
+        const variantName = await this.getTranslatedVariantName(ctx, variant);
+        return `Maximum order quantity for "${variantName}" is ${max}`;
+    }
+
+    private async getTranslatedVariantName(ctx: RequestContext, variant: ProductVariant) {
+        await this.entityHydrator.hydrate(ctx, variant, { relations: ['translations'] });
+        const translated = this.translatorService.translate(variant, ctx);
+        return translated.name;
+    }
+}
+
+/**
+ * This plugin enforces minimum and maximum order quantities on ProductVariants.
+ * It adds two new custom fields to ProductVariant:
+ * - minOrderQuantity
+ * - maxOrderQuantity
+ *
+ * It also adds an OrderInterceptor which enforces these limits.
+ */
+@VendurePlugin({
+    configuration: config => {
+        config.customFields.ProductVariant.push({
+            type: 'int',
+            min: 0,
+            name: 'minOrderQuantity',
+            label: [{ languageCode: LanguageCode.en, value: 'Minimum order quantity' }],
+            nullable: true,
+        });
+        config.customFields.ProductVariant.push({
+            type: 'int',
+            min: 0,
+            name: 'maxOrderQuantity',
+            label: [{ languageCode: LanguageCode.en, value: 'Maximum order quantity' }],
+            nullable: true,
+        });
+        config.orderOptions.orderInterceptors.push(new MinMaxOrderInterceptor());
+        return config;
+    },
+})
+export class OrderQuantityLimitsPlugin {}

+ 13 - 0
packages/dev-server/example-plugins/product-bundles/README.md

@@ -0,0 +1,13 @@
+# Product Bundle Plugin
+
+This is an example implementation which adds support for product bundles.
+
+This plugin uses a couple of features added in Vendure v3.1:
+
+- Custom field "struct" types
+- The OrderInterceptor API
+
+## Status
+
+It is currently a work-in-progress: the Admin UI parts will be completed once we add
+support for config-based list/detail views.

+ 73 - 0
packages/dev-server/example-plugins/product-bundles/api/api-extensions.ts

@@ -0,0 +1,73 @@
+import gql from 'graphql-tag';
+
+const productBundleAdminApiExtensions = gql`
+    type ProductBundle implements Node {
+        id: ID!
+        createdAt: DateTime!
+        updatedAt: DateTime!
+        name: String!
+        description: String!
+    }
+
+    type ProductBundleItem {
+        productVariant: ProductVariant!
+        price: Money!
+        quantity: Int!
+    }
+
+    type ProductBundleList implements PaginatedList {
+        items: [ProductBundle!]!
+        totalItems: Int!
+    }
+
+    # Generated at run-time by Vendure
+    input ProductBundleListOptions
+
+    extend type Query {
+        productBundle(id: ID!): ProductBundle
+        productBundles(options: ProductBundleListOptions): ProductBundleList!
+    }
+
+    input CreateProductBundleInput {
+        name: String!
+        description: String!
+    }
+
+    input UpdateProductBundleInput {
+        id: ID!
+        name: String
+        description: String
+    }
+
+    input CreateProductBundleItemInput {
+        bundleId: ID!
+        productVariantId: ID!
+        price: Money!
+        quantity: Int!
+    }
+
+    input UpdateProductBundleItemInput {
+        id: ID!
+        price: Money
+        quantity: Int
+    }
+
+    extend type Mutation {
+        createProductBundle(input: CreateProductBundleInput!): ProductBundle!
+        updateProductBundle(input: UpdateProductBundleInput!): ProductBundle!
+        deleteProductBundle(id: ID!): DeletionResponse!
+        createProductBundleItem(input: CreateProductBundleItemInput!): ProductBundleItem!
+        updateProductBundleItem(input: UpdateProductBundleItemInput!): ProductBundleItem!
+        deleteProductBundleItem(id: ID!): DeletionResponse!
+    }
+`;
+export const adminApiExtensions = gql`
+    ${productBundleAdminApiExtensions}
+`;
+
+export const shopApiExtensions = gql`
+    extend type Mutation {
+        addProductBundleToOrder(bundleId: ID!): UpdateOrderItemsResult!
+        removeProductBundleFromOrder(bundleId: ID!): RemoveOrderItemsResult!
+    }
+`;

+ 108 - 0
packages/dev-server/example-plugins/product-bundles/api/product-bundle-admin.resolver.ts

@@ -0,0 +1,108 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { DeletionResponse, Permission } from '@vendure/common/lib/generated-types';
+import {
+    Allow,
+    Ctx,
+    ID,
+    ListQueryOptions,
+    PaginatedList,
+    RelationPaths,
+    Relations,
+    RequestContext,
+    Transaction,
+} from '@vendure/core';
+import { productBundlePermission } from '../constants';
+import { ProductBundle } from '../entities/product-bundle.entity';
+import { ProductBundleItemService } from '../services/product-bundle-item.service';
+import { ProductBundleService } from '../services/product-bundle.service';
+import {
+    CreateProductBundleInput,
+    CreateProductBundleItemInput,
+    UpdateProductBundleInput,
+    UpdateProductBundleItemInput,
+} from '../types';
+
+@Resolver()
+export class ProductBundleAdminResolver {
+    constructor(
+        private productBundleService: ProductBundleService,
+        private productBundleItemService: ProductBundleItemService,
+    ) {}
+
+    @Query()
+    @Allow(productBundlePermission.Read)
+    async productBundle(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { id: ID },
+        @Relations(ProductBundle) relations: RelationPaths<ProductBundle>,
+    ): Promise<ProductBundle | null> {
+        return this.productBundleService.findOne(ctx, args.id, relations);
+    }
+
+    @Query()
+    @Allow(productBundlePermission.Read)
+    async productBundles(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { options: ListQueryOptions<ProductBundle> },
+        @Relations(ProductBundle) relations: RelationPaths<ProductBundle>,
+    ): Promise<PaginatedList<ProductBundle>> {
+        return this.productBundleService.findAll(ctx, args.options || undefined, relations);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Create)
+    async createProductBundle(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: CreateProductBundleInput },
+    ): Promise<ProductBundle> {
+        return this.productBundleService.create(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Update)
+    async updateProductBundle(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: UpdateProductBundleInput },
+    ): Promise<ProductBundle> {
+        return this.productBundleService.update(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Delete)
+    async deleteProductBundle(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { id: ID },
+    ): Promise<DeletionResponse> {
+        return this.productBundleService.delete(ctx, args.id);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Create)
+    createProductBundleItem(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: CreateProductBundleItemInput },
+    ) {
+        return this.productBundleItemService.createProductBundleItem(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Update)
+    updateProductBundleItem(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { input: UpdateProductBundleItemInput },
+    ) {
+        return this.productBundleItemService.updateProductBundleItem(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(productBundlePermission.Delete)
+    deleteProductBundleItem(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
+        return this.productBundleItemService.delete(ctx, args.id);
+    }
+}

+ 32 - 0
packages/dev-server/example-plugins/product-bundles/api/product-bundle-shop.resolver.ts

@@ -0,0 +1,32 @@
+import { Args, Mutation, Resolver } from '@nestjs/graphql';
+import { UpdateOrderItemsResult } from '@vendure/common/lib/generated-shop-types';
+import { ID } from '@vendure/common/lib/shared-types';
+import {
+    ACTIVE_ORDER_INPUT_FIELD_NAME,
+    ActiveOrderService,
+    Ctx,
+    ErrorResultUnion,
+    Order,
+    RequestContext,
+    Transaction,
+} from '@vendure/core';
+
+import { ProductBundleService } from '../services/product-bundle.service';
+
+@Resolver()
+export class ProductBundleShopResolver {
+    constructor(
+        private productBundleService: ProductBundleService,
+        private activeOrderService: ActiveOrderService,
+    ) {}
+
+    @Mutation()
+    @Transaction()
+    async addProductBundleToOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: { productId: ID },
+    ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
+        const order = await this.activeOrderService.getActiveOrder(ctx, undefined, true);
+        return this.productBundleService.addProductBundleToOrder(ctx, order, args.productId);
+    }
+}

+ 17 - 0
packages/dev-server/example-plugins/product-bundles/config/bundle-order-interceptor.ts

@@ -0,0 +1,17 @@
+import { Order, OrderInterceptor, OrderLine, RequestContext, WillAdjustOrderLineInput } from '@vendure/core';
+
+export class BundleOrderInterceptor implements OrderInterceptor {
+    willAdjustOrderLine(ctx: RequestContext, order: Order, input: WillAdjustOrderLineInput) {
+        if (input.orderLine.customFields.fromBundle) {
+            return 'Cannot adjust bundle items';
+        }
+        return;
+    }
+
+    willRemoveItemFromOrder(ctx: RequestContext, order: Order, orderLine: OrderLine) {
+        if (orderLine.customFields.fromBundle) {
+            return 'Cannot remove bundle items';
+        }
+        return;
+    }
+}

+ 4 - 0
packages/dev-server/example-plugins/product-bundles/constants.ts

@@ -0,0 +1,4 @@
+import { CrudPermissionDefinition } from '@vendure/core';
+
+export const loggerCtx = 'ProductBundlesPlugin';
+export const productBundlePermission = new CrudPermissionDefinition('ProductBundle');

+ 29 - 0
packages/dev-server/example-plugins/product-bundles/entities/product-bundle-item.entity.ts

@@ -0,0 +1,29 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { EntityId, ID, Money, ProductVariant, VendureEntity } from '@vendure/core';
+import { Column, Entity, ManyToOne } from 'typeorm';
+import { ProductBundle } from './product-bundle.entity';
+
+@Entity()
+export class ProductBundleItem extends VendureEntity {
+    constructor(input?: DeepPartial<ProductBundleItem>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant)
+    productVariant: ProductVariant;
+
+    @EntityId()
+    productVariantId: ID;
+
+    @ManyToOne(type => ProductBundle, bundle => bundle.items)
+    bundle: ProductBundle;
+
+    @EntityId()
+    bundleId: ID;
+
+    @Money()
+    price: number;
+
+    @Column()
+    quantity: number;
+}

+ 20 - 0
packages/dev-server/example-plugins/product-bundles/entities/product-bundle.entity.ts

@@ -0,0 +1,20 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { VendureEntity, Product, EntityId, ID, ProductVariant } from '@vendure/core';
+import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
+import { ProductBundleItem } from './product-bundle-item.entity';
+
+@Entity()
+export class ProductBundle extends VendureEntity {
+    constructor(input?: DeepPartial<ProductBundle>) {
+        super(input);
+    }
+
+    @OneToMany(type => ProductBundleItem, item => item.bundle)
+    items: ProductBundleItem[];
+
+    @Column()
+    name: string;
+
+    @Column()
+    description: string;
+}

+ 48 - 0
packages/dev-server/example-plugins/product-bundles/product-bundles.plugin.ts

@@ -0,0 +1,48 @@
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
+import path from 'path';
+
+import { adminApiExtensions, shopApiExtensions } from './api/api-extensions';
+import { ProductBundleAdminResolver } from './api/product-bundle-admin.resolver';
+import { ProductBundleShopResolver } from './api/product-bundle-shop.resolver';
+import { BundleOrderInterceptor } from './config/bundle-order-interceptor';
+import { productBundlePermission } from './constants';
+import { ProductBundleItem } from './entities/product-bundle-item.entity';
+import { ProductBundle } from './entities/product-bundle.entity';
+import { ProductBundleItemService } from './services/product-bundle-item.service';
+import { ProductBundleService } from './services/product-bundle.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [ProductBundle, ProductBundleItem],
+    configuration: config => {
+        config.customFields.OrderLine.push({
+            type: 'struct',
+            name: 'fromBundle',
+            fields: [
+                { name: 'bundleId', type: 'string' },
+                { name: 'bundleName', type: 'string' },
+            ],
+        });
+        config.orderOptions.orderInterceptors.push(new BundleOrderInterceptor());
+        config.authOptions.customPermissions.push(productBundlePermission);
+        return config;
+    },
+    providers: [ProductBundleService, ProductBundleItemService],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [ProductBundleAdminResolver],
+    },
+    shopApiExtensions: {
+        schema: shopApiExtensions,
+        resolvers: [ProductBundleShopResolver],
+    },
+})
+export class ProductBundlesPlugin {
+    static uiExtensions: AdminUiExtension = {
+        id: 'product-bundles',
+        extensionPath: path.join(__dirname, 'ui'),
+        routes: [{ route: 'product-bundles', filePath: 'routes.ts' }],
+        providers: ['providers.ts'],
+    };
+}

+ 56 - 0
packages/dev-server/example-plugins/product-bundles/services/product-bundle-item.service.ts

@@ -0,0 +1,56 @@
+import { Injectable } from '@nestjs/common';
+import { DeletionResponse, DeletionResult } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+import { ListQueryBuilder, patchEntity, RequestContext, TransactionalConnection } from '@vendure/core';
+
+import { ProductBundleItem } from '../entities/product-bundle-item.entity';
+import { ProductBundle } from '../entities/product-bundle.entity';
+import { CreateProductBundleItemInput, UpdateProductBundleItemInput } from '../types';
+
+@Injectable()
+export class ProductBundleItemService {
+    constructor(
+        private connection: TransactionalConnection,
+        private listQueryBuilder: ListQueryBuilder,
+    ) {}
+
+    async createProductBundleItem(
+        ctx: RequestContext,
+        input: CreateProductBundleItemInput,
+    ): Promise<ProductBundleItem> {
+        const bundle = await this.connection.getEntityOrThrow(ctx, ProductBundle, input.bundleId);
+        const newEntity = await this.connection.getRepository(ctx, ProductBundleItem).save(
+            new ProductBundleItem({
+                ...input,
+                bundle,
+            }),
+        );
+
+        return newEntity;
+    }
+
+    async updateProductBundleItem(
+        ctx: RequestContext,
+        input: UpdateProductBundleItemInput,
+    ): Promise<ProductBundleItem> {
+        const entity = await this.connection.getEntityOrThrow(ctx, ProductBundleItem, input.id);
+        const updatedEntity = patchEntity(entity, input);
+        await this.connection.getRepository(ctx, ProductBundleItem).save(updatedEntity, { reload: false });
+        return updatedEntity;
+    }
+
+    async delete(ctx: RequestContext, id: ID): Promise<DeletionResponse> {
+        const entity = await this.connection.getEntityOrThrow(ctx, ProductBundleItem, id);
+        try {
+            await this.connection.getRepository(ctx, ProductBundleItem).remove(entity);
+            return {
+                result: DeletionResult.DELETED,
+            };
+        } catch (e: any) {
+            return {
+                result: DeletionResult.NOT_DELETED,
+                message: e.toString(),
+            };
+        }
+    }
+}

Some files were not shown because too many files changed in this diff