ソースを参照

feat(core): Improved error handling for ShopAPI order resolvers

Relates to #437.
Michael Bromley 5 年 前
コミット
156c9e2938
57 ファイル変更3082 行追加1791 行削除
  1. 2 0
      .prettierignore
  2. 26 112
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  3. 299 27
      packages/common/src/generated-shop-types.ts
  4. 23 4
      packages/common/src/generated-types.ts
  5. 4 3
      packages/core/e2e/asset.e2e-spec.ts
  6. 7 2
      packages/core/e2e/authentication-strategy.e2e-spec.ts
  7. 11 4
      packages/core/e2e/customer.e2e-spec.ts
  8. 4 3
      packages/core/e2e/fixtures/test-payment-methods.ts
  9. 2 0
      packages/core/e2e/graphql/fragments.ts
  10. 57 186
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  11. 233 109
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  12. 9 3
      packages/core/e2e/graphql/shared-definitions.ts
  13. 195 53
      packages/core/e2e/graphql/shop-definitions.ts
  14. 13 2
      packages/core/e2e/order-channel.e2e-spec.ts
  15. 114 91
      packages/core/e2e/order-process.e2e-spec.ts
  16. 74 54
      packages/core/e2e/order-promotion.e2e-spec.ts
  17. 13 11
      packages/core/e2e/price-calculation-strategy.e2e-spec.ts
  18. 311 216
      packages/core/e2e/shop-auth.e2e-spec.ts
  19. 31 21
      packages/core/e2e/shop-customer.e2e-spec.ts
  20. 294 205
      packages/core/e2e/shop-order.e2e-spec.ts
  21. 7 0
      packages/core/src/api/api.module.ts
  22. 5 1
      packages/core/src/api/config/generate-error-code-enum.ts
  23. 40 0
      packages/core/src/api/middleware/translate-error-result-interceptor.ts
  24. 0 5
      packages/core/src/api/resolvers/admin/asset.resolver.ts
  25. 4 2
      packages/core/src/api/resolvers/admin/customer.resolver.ts
  26. 4 3
      packages/core/src/api/resolvers/base/base-auth.resolver.ts
  27. 130 64
      packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
  28. 4 3
      packages/core/src/api/resolvers/shop/shop-customer.resolver.ts
  29. 35 12
      packages/core/src/api/resolvers/shop/shop-order.resolver.ts
  30. 1 1
      packages/core/src/api/schema/admin-api/customer.api.graphql
  31. 3 1
      packages/core/src/api/schema/admin-api/order.api.graphql
  32. 17 1
      packages/core/src/api/schema/common/common-types.graphql
  33. 215 19
      packages/core/src/api/schema/shop-api/shop.api.graphql
  34. 56 0
      packages/core/src/common/error/error-result.ts
  35. 0 152
      packages/core/src/common/error/errors.ts
  36. 47 30
      packages/core/src/common/error/generated-graphql-admin-errors.ts
  37. 328 10
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  38. 1 1
      packages/core/src/common/types/common-types.ts
  39. 1 1
      packages/core/src/common/utils.ts
  40. 16 0
      packages/core/src/config/payment-method/payment-method-handler.ts
  41. 16 0
      packages/core/src/i18n/i18n.service.ts
  42. 37 25
      packages/core/src/i18n/messages/en.json
  43. 1 1
      packages/core/src/i18n/messages/es.json
  44. 1 1
      packages/core/src/i18n/messages/pt_BR.json
  45. 10 10
      packages/core/src/service/helpers/order-state-machine/order-state-machine.ts
  46. 9 18
      packages/core/src/service/services/asset.service.ts
  47. 7 2
      packages/core/src/service/services/auth.service.ts
  48. 78 44
      packages/core/src/service/services/customer.service.ts
  49. 107 36
      packages/core/src/service/services/order.service.ts
  50. 12 6
      packages/core/src/service/services/promotion.service.ts
  51. 38 26
      packages/core/src/service/services/user.service.ts
  52. 1 1
      packages/dev-server/dev-config.ts
  53. 61 169
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  54. 0 0
      schema-admin.json
  55. 0 0
      schema-shop.json
  56. 28 24
      scripts/codegen/generate-graphql-types.ts
  57. 40 16
      scripts/codegen/plugins/graphql-errors-plugin.ts

+ 2 - 0
.prettierignore

@@ -1,3 +1,5 @@
 generated-types.ts
 lazy-extensions.module.ts
 shared-extensions.module.ts
+generated-graphql-shop-errors.ts
+generated-graphql-admin-errors.ts

+ 26 - 112
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -29,7 +29,6 @@ export type AddNoteToOrderInput = {
 };
 
 export type Address = Node & {
-    __typename?: 'Address';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -48,7 +47,6 @@ export type Address = Node & {
 };
 
 export type Adjustment = {
-    __typename?: 'Adjustment';
     adjustmentSource: Scalars['String'];
     type: AdjustmentType;
     description: Scalars['String'];
@@ -66,7 +64,6 @@ export enum AdjustmentType {
 }
 
 export type Administrator = Node & {
-    __typename?: 'Administrator';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -85,7 +82,6 @@ export type AdministratorFilterParameter = {
 };
 
 export type AdministratorList = PaginatedList & {
-    __typename?: 'AdministratorList';
     items: Array<Administrator>;
     totalItems: Scalars['Int'];
 };
@@ -107,7 +103,6 @@ export type AdministratorSortParameter = {
 };
 
 export type Asset = Node & {
-    __typename?: 'Asset';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -136,7 +131,6 @@ export type AssetFilterParameter = {
 };
 
 export type AssetList = PaginatedList & {
-    __typename?: 'AssetList';
     items: Array<Asset>;
     totalItems: Scalars['Int'];
 };
@@ -178,7 +172,6 @@ export type AuthenticationInput = {
 };
 
 export type AuthenticationMethod = Node & {
-    __typename?: 'AuthenticationMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -186,7 +179,6 @@ export type AuthenticationMethod = Node & {
 };
 
 export type BooleanCustomFieldConfig = CustomField & {
-    __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -202,7 +194,6 @@ export type BooleanOperators = {
 
 export type Cancellation = Node &
     StockMovement & {
-        __typename?: 'Cancellation';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -221,7 +212,6 @@ export type CancelOrderInput = {
 };
 
 export type Channel = Node & {
-    __typename?: 'Channel';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -235,7 +225,6 @@ export type Channel = Node & {
 };
 
 export type Collection = Node & {
-    __typename?: 'Collection';
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -261,7 +250,6 @@ export type CollectionProductVariantsArgs = {
 };
 
 export type CollectionBreadcrumb = {
-    __typename?: 'CollectionBreadcrumb';
     id: Scalars['ID'];
     name: Scalars['String'];
     slug: Scalars['String'];
@@ -279,7 +267,6 @@ export type CollectionFilterParameter = {
 };
 
 export type CollectionList = PaginatedList & {
-    __typename?: 'CollectionList';
     items: Array<Collection>;
     totalItems: Scalars['Int'];
 };
@@ -302,7 +289,6 @@ export type CollectionSortParameter = {
 };
 
 export type CollectionTranslation = {
-    __typename?: 'CollectionTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -313,13 +299,11 @@ export type CollectionTranslation = {
 };
 
 export type ConfigArg = {
-    __typename?: 'ConfigArg';
     name: Scalars['String'];
     value: Scalars['String'];
 };
 
 export type ConfigArgDefinition = {
-    __typename?: 'ConfigArgDefinition';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -334,13 +318,11 @@ export type ConfigArgInput = {
 };
 
 export type ConfigurableOperation = {
-    __typename?: 'ConfigurableOperation';
     code: Scalars['String'];
     args: Array<ConfigArg>;
 };
 
 export type ConfigurableOperationDefinition = {
-    __typename?: 'ConfigurableOperationDefinition';
     code: Scalars['String'];
     args: Array<ConfigArgDefinition>;
     description: Scalars['String'];
@@ -352,7 +334,6 @@ export type ConfigurableOperationInput = {
 };
 
 export type Coordinate = {
-    __typename?: 'Coordinate';
     x: Scalars['Float'];
     y: Scalars['Float'];
 };
@@ -363,7 +344,6 @@ export type CoordinateInput = {
 };
 
 export type Country = Node & {
-    __typename?: 'Country';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -384,7 +364,6 @@ export type CountryFilterParameter = {
 };
 
 export type CountryList = PaginatedList & {
-    __typename?: 'CountryList';
     items: Array<Country>;
     totalItems: Scalars['Int'];
 };
@@ -405,7 +384,6 @@ export type CountrySortParameter = {
 };
 
 export type CountryTranslation = {
-    __typename?: 'CountryTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -932,14 +910,12 @@ export enum CurrencyCode {
 }
 
 export type CurrentUser = {
-    __typename?: 'CurrentUser';
     id: Scalars['ID'];
     identifier: Scalars['String'];
     channels: Array<CurrentUserChannel>;
 };
 
 export type CurrentUserChannel = {
-    __typename?: 'CurrentUserChannel';
     id: Scalars['ID'];
     token: Scalars['String'];
     code: Scalars['String'];
@@ -947,7 +923,6 @@ export type CurrentUserChannel = {
 };
 
 export type Customer = Node & {
-    __typename?: 'Customer';
     groups: Array<CustomerGroup>;
     history: HistoryEntryList;
     id: Scalars['ID'];
@@ -983,7 +958,6 @@ export type CustomerFilterParameter = {
 };
 
 export type CustomerGroup = Node & {
-    __typename?: 'CustomerGroup';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1002,7 +976,6 @@ export type CustomerGroupFilterParameter = {
 };
 
 export type CustomerGroupList = PaginatedList & {
-    __typename?: 'CustomerGroupList';
     items: Array<CustomerGroup>;
     totalItems: Scalars['Int'];
 };
@@ -1022,7 +995,6 @@ export type CustomerGroupSortParameter = {
 };
 
 export type CustomerList = PaginatedList & {
-    __typename?: 'CustomerList';
     items: Array<Customer>;
     totalItems: Scalars['Int'];
 };
@@ -1064,7 +1036,6 @@ export type CustomFieldConfig =
     | DateTimeCustomFieldConfig;
 
 export type CustomFields = {
-    __typename?: 'CustomFields';
     Address: Array<CustomFieldConfig>;
     Collection: Array<CustomFieldConfig>;
     Customer: Array<CustomFieldConfig>;
@@ -1098,7 +1069,6 @@ export type DateRange = {
  * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#Additional_attributes
  */
 export type DateTimeCustomFieldConfig = CustomField & {
-    __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1112,7 +1082,6 @@ export type DateTimeCustomFieldConfig = CustomField & {
 };
 
 export type DeletionResponse = {
-    __typename?: 'DeletionResponse';
     result: DeletionResult;
     message?: Maybe<Scalars['String']>;
 };
@@ -1125,8 +1094,9 @@ export enum DeletionResult {
 }
 
 export enum ErrorCode {
-    UnknownError = 'UnknownError',
-    MimeTypeError = 'MimeTypeError',
+    UNKNOWN_ERROR = 'UNKNOWN_ERROR',
+    MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
+    ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
 }
 
 export type ErrorResult = {
@@ -1135,7 +1105,6 @@ export type ErrorResult = {
 };
 
 export type Facet = Node & {
-    __typename?: 'Facet';
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -1158,7 +1127,6 @@ export type FacetFilterParameter = {
 };
 
 export type FacetList = PaginatedList & {
-    __typename?: 'FacetList';
     items: Array<Facet>;
     totalItems: Scalars['Int'];
 };
@@ -1179,7 +1147,6 @@ export type FacetSortParameter = {
 };
 
 export type FacetTranslation = {
-    __typename?: 'FacetTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1195,7 +1162,6 @@ export type FacetTranslationInput = {
 };
 
 export type FacetValue = Node & {
-    __typename?: 'FacetValue';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1212,13 +1178,11 @@ export type FacetValue = Node & {
  * by the search, and in what quantity.
  */
 export type FacetValueResult = {
-    __typename?: 'FacetValueResult';
     facetValue: FacetValue;
     count: Scalars['Int'];
 };
 
 export type FacetValueTranslation = {
-    __typename?: 'FacetValueTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1234,7 +1198,6 @@ export type FacetValueTranslationInput = {
 };
 
 export type FloatCustomFieldConfig = CustomField & {
-    __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1248,7 +1211,6 @@ export type FloatCustomFieldConfig = CustomField & {
 };
 
 export type Fulfillment = Node & {
-    __typename?: 'Fulfillment';
     nextStates: Array<Scalars['String']>;
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -1266,7 +1228,6 @@ export type FulfillOrderInput = {
 };
 
 export type GlobalSettings = {
-    __typename?: 'GlobalSettings';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1277,7 +1238,6 @@ export type GlobalSettings = {
 };
 
 export type HistoryEntry = Node & {
-    __typename?: 'HistoryEntry';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1295,7 +1255,6 @@ export type HistoryEntryFilterParameter = {
 };
 
 export type HistoryEntryList = PaginatedList & {
-    __typename?: 'HistoryEntryList';
     items: Array<HistoryEntry>;
     totalItems: Scalars['Int'];
 };
@@ -1340,14 +1299,12 @@ export enum HistoryEntryType {
 }
 
 export type ImportInfo = {
-    __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
     processed: Scalars['Int'];
     imported: Scalars['Int'];
 };
 
 export type IntCustomFieldConfig = CustomField & {
-    __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1361,7 +1318,6 @@ export type IntCustomFieldConfig = CustomField & {
 };
 
 export type Job = Node & {
-    __typename?: 'Job';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     startedAt?: Maybe<Scalars['DateTime']>;
@@ -1388,7 +1344,6 @@ export type JobFilterParameter = {
 };
 
 export type JobList = PaginatedList & {
-    __typename?: 'JobList';
     items: Array<Job>;
     totalItems: Scalars['Int'];
 };
@@ -1401,7 +1356,6 @@ export type JobListOptions = {
 };
 
 export type JobQueue = {
-    __typename?: 'JobQueue';
     name: Scalars['String'];
     running: Scalars['Boolean'];
 };
@@ -1757,7 +1711,6 @@ export enum LanguageCode {
 }
 
 export type LocaleStringCustomFieldConfig = CustomField & {
-    __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1770,7 +1723,6 @@ export type LocaleStringCustomFieldConfig = CustomField & {
 };
 
 export type LocalizedString = {
-    __typename?: 'LocalizedString';
     languageCode: LanguageCode;
     value: Scalars['String'];
 };
@@ -1781,12 +1733,10 @@ export enum LogicalOperator {
 }
 
 export type LoginResult = {
-    __typename?: 'LoginResult';
     user: CurrentUser;
 };
 
 export type MimeTypeError = ErrorResult & {
-    __typename?: 'MimeTypeError';
     code: ErrorCode;
     message: Scalars['String'];
     fileName: Scalars['String'];
@@ -1800,7 +1750,6 @@ export type MoveCollectionInput = {
 };
 
 export type Mutation = {
-    __typename?: 'Mutation';
     /** Create a new Administrator */
     createAdministrator: Administrator;
     /** Update an existing Administrator */
@@ -1866,7 +1815,7 @@ export type Mutation = {
     /** Update an existing Address */
     updateCustomerAddress: Address;
     /** Update an existing Address */
-    deleteCustomerAddress: Scalars['Boolean'];
+    deleteCustomerAddress: Success;
     addNoteToCustomer: Customer;
     updateCustomerNote: HistoryEntry;
     deleteCustomerNote: DeletionResponse;
@@ -1894,7 +1843,7 @@ export type Mutation = {
     addNoteToOrder: Order;
     updateOrderNote: HistoryEntry;
     deleteOrderNote: DeletionResponse;
-    transitionOrderToState?: Maybe<Order>;
+    transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionFulfillmentToState: Fulfillment;
     setOrderCustomFields?: Maybe<Order>;
     /** Update an existing PaymentMethod */
@@ -2366,7 +2315,6 @@ export type NumberRange = {
 };
 
 export type Order = Node & {
-    __typename?: 'Order';
     nextStates: Array<Scalars['String']>;
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -2405,7 +2353,6 @@ export type OrderHistoryArgs = {
 };
 
 export type OrderAddress = {
-    __typename?: 'OrderAddress';
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
     streetLine1?: Maybe<Scalars['String']>;
@@ -2434,7 +2381,6 @@ export type OrderFilterParameter = {
 };
 
 export type OrderItem = Node & {
-    __typename?: 'OrderItem';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2449,7 +2395,6 @@ export type OrderItem = Node & {
 };
 
 export type OrderLine = Node & {
-    __typename?: 'OrderLine';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2471,7 +2416,6 @@ export type OrderLineInput = {
 };
 
 export type OrderList = PaginatedList & {
-    __typename?: 'OrderList';
     items: Array<Order>;
     totalItems: Scalars['Int'];
 };
@@ -2484,7 +2428,6 @@ export type OrderListOptions = {
 };
 
 export type OrderProcessState = {
-    __typename?: 'OrderProcessState';
     name: Scalars['String'];
     to: Array<Scalars['String']>;
 };
@@ -2503,13 +2446,21 @@ export type OrderSortParameter = {
     total?: Maybe<SortOrder>;
 };
 
+/** Returned if there is an error in transitioning the Order state */
+export type OrderStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 export type PaginatedList = {
     items: Array<Node>;
     totalItems: Scalars['Int'];
 };
 
 export type Payment = Node & {
-    __typename?: 'Payment';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2523,7 +2474,6 @@ export type Payment = Node & {
 };
 
 export type PaymentMethod = Node & {
-    __typename?: 'PaymentMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2541,7 +2491,6 @@ export type PaymentMethodFilterParameter = {
 };
 
 export type PaymentMethodList = PaginatedList & {
-    __typename?: 'PaymentMethodList';
     items: Array<PaymentMethod>;
     totalItems: Scalars['Int'];
 };
@@ -2605,13 +2554,11 @@ export enum Permission {
 
 /** The price range where the result has more than one price */
 export type PriceRange = {
-    __typename?: 'PriceRange';
     min: Scalars['Int'];
     max: Scalars['Int'];
 };
 
 export type Product = Node & {
-    __typename?: 'Product';
     enabled: Scalars['Boolean'];
     channels: Array<Channel>;
     id: Scalars['ID'];
@@ -2642,7 +2589,6 @@ export type ProductFilterParameter = {
 };
 
 export type ProductList = PaginatedList & {
-    __typename?: 'ProductList';
     items: Array<Product>;
     totalItems: Scalars['Int'];
 };
@@ -2655,7 +2601,6 @@ export type ProductListOptions = {
 };
 
 export type ProductOption = Node & {
-    __typename?: 'ProductOption';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2669,7 +2614,6 @@ export type ProductOption = Node & {
 };
 
 export type ProductOptionGroup = Node & {
-    __typename?: 'ProductOptionGroup';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2682,7 +2626,6 @@ export type ProductOptionGroup = Node & {
 };
 
 export type ProductOptionGroupTranslation = {
-    __typename?: 'ProductOptionGroupTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2698,7 +2641,6 @@ export type ProductOptionGroupTranslationInput = {
 };
 
 export type ProductOptionTranslation = {
-    __typename?: 'ProductOptionTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2723,7 +2665,6 @@ export type ProductSortParameter = {
 };
 
 export type ProductTranslation = {
-    __typename?: 'ProductTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2743,7 +2684,6 @@ export type ProductTranslationInput = {
 };
 
 export type ProductVariant = Node & {
-    __typename?: 'ProductVariant';
     enabled: Scalars['Boolean'];
     stockOnHand: Scalars['Int'];
     trackInventory: Scalars['Boolean'];
@@ -2790,7 +2730,6 @@ export type ProductVariantFilterParameter = {
 };
 
 export type ProductVariantList = PaginatedList & {
-    __typename?: 'ProductVariantList';
     items: Array<ProductVariant>;
     totalItems: Scalars['Int'];
 };
@@ -2815,7 +2754,6 @@ export type ProductVariantSortParameter = {
 };
 
 export type ProductVariantTranslation = {
-    __typename?: 'ProductVariantTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2831,7 +2769,6 @@ export type ProductVariantTranslationInput = {
 };
 
 export type Promotion = Node & {
-    __typename?: 'Promotion';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2857,7 +2794,6 @@ export type PromotionFilterParameter = {
 };
 
 export type PromotionList = PaginatedList & {
-    __typename?: 'PromotionList';
     items: Array<Promotion>;
     totalItems: Scalars['Int'];
 };
@@ -2881,7 +2817,6 @@ export type PromotionSortParameter = {
 };
 
 export type Query = {
-    __typename?: 'Query';
     administrators: AdministratorList;
     administrator?: Maybe<Administrator>;
     /** Get a list of Assets */
@@ -3104,7 +3039,6 @@ export type QueryZoneArgs = {
 };
 
 export type Refund = Node & {
-    __typename?: 'Refund';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3136,7 +3070,6 @@ export type RemoveProductsFromChannelInput = {
 
 export type Return = Node &
     StockMovement & {
-        __typename?: 'Return';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3147,7 +3080,6 @@ export type Return = Node &
     };
 
 export type Role = Node & {
-    __typename?: 'Role';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3165,7 +3097,6 @@ export type RoleFilterParameter = {
 };
 
 export type RoleList = PaginatedList & {
-    __typename?: 'RoleList';
     items: Array<Role>;
     totalItems: Scalars['Int'];
 };
@@ -3187,7 +3118,6 @@ export type RoleSortParameter = {
 
 export type Sale = Node &
     StockMovement & {
-        __typename?: 'Sale';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3210,19 +3140,16 @@ export type SearchInput = {
 };
 
 export type SearchReindexResponse = {
-    __typename?: 'SearchReindexResponse';
     success: Scalars['Boolean'];
 };
 
 export type SearchResponse = {
-    __typename?: 'SearchResponse';
     items: Array<SearchResult>;
     totalItems: Scalars['Int'];
     facetValues: Array<FacetValueResult>;
 };
 
 export type SearchResult = {
-    __typename?: 'SearchResult';
     enabled: Scalars['Boolean'];
     /** An array of ids of the Collections in which this result appears */
     channelIds: Array<Scalars['ID']>;
@@ -3251,7 +3178,6 @@ export type SearchResult = {
 };
 
 export type SearchResultAsset = {
-    __typename?: 'SearchResultAsset';
     id: Scalars['ID'];
     preview: Scalars['String'];
     focalPoint?: Maybe<Coordinate>;
@@ -3266,7 +3192,6 @@ export type SearchResultSortParameter = {
 };
 
 export type ServerConfig = {
-    __typename?: 'ServerConfig';
     orderProcess: Array<OrderProcessState>;
     permittedAssetTypes: Array<Scalars['String']>;
     customFieldConfig: CustomFields;
@@ -3278,7 +3203,6 @@ export type SettleRefundInput = {
 };
 
 export type ShippingMethod = Node & {
-    __typename?: 'ShippingMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3297,7 +3221,6 @@ export type ShippingMethodFilterParameter = {
 };
 
 export type ShippingMethodList = PaginatedList & {
-    __typename?: 'ShippingMethodList';
     items: Array<ShippingMethod>;
     totalItems: Scalars['Int'];
 };
@@ -3310,7 +3233,6 @@ export type ShippingMethodListOptions = {
 };
 
 export type ShippingMethodQuote = {
-    __typename?: 'ShippingMethodQuote';
     id: Scalars['ID'];
     price: Scalars['Int'];
     priceWithTax: Scalars['Int'];
@@ -3328,7 +3250,6 @@ export type ShippingMethodSortParameter = {
 
 /** The price value where the result has a single price */
 export type SinglePrice = {
-    __typename?: 'SinglePrice';
     value: Scalars['Int'];
 };
 
@@ -3339,7 +3260,6 @@ export enum SortOrder {
 
 export type StockAdjustment = Node &
     StockMovement & {
-        __typename?: 'StockAdjustment';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3360,7 +3280,6 @@ export type StockMovement = {
 export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
 
 export type StockMovementList = {
-    __typename?: 'StockMovementList';
     items: Array<StockMovementItem>;
     totalItems: Scalars['Int'];
 };
@@ -3379,7 +3298,6 @@ export enum StockMovementType {
 }
 
 export type StringCustomFieldConfig = CustomField & {
-    __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -3393,7 +3311,6 @@ export type StringCustomFieldConfig = CustomField & {
 };
 
 export type StringFieldOption = {
-    __typename?: 'StringFieldOption';
     value: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
 };
@@ -3403,8 +3320,12 @@ export type StringOperators = {
     contains?: Maybe<Scalars['String']>;
 };
 
+/** Indicates that an operation succeeded, where we do not want to return any more specific information. */
+export type Success = {
+    success: Scalars['Boolean'];
+};
+
 export type TaxCategory = Node & {
-    __typename?: 'TaxCategory';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3412,7 +3333,6 @@ export type TaxCategory = Node & {
 };
 
 export type TaxRate = Node & {
-    __typename?: 'TaxRate';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3433,7 +3353,6 @@ export type TaxRateFilterParameter = {
 };
 
 export type TaxRateList = PaginatedList & {
-    __typename?: 'TaxRateList';
     items: Array<TaxRate>;
     totalItems: Scalars['Int'];
 };
@@ -3471,7 +3390,6 @@ export type TestShippingMethodOrderLineInput = {
 };
 
 export type TestShippingMethodQuote = {
-    __typename?: 'TestShippingMethodQuote';
     price: Scalars['Int'];
     priceWithTax: Scalars['Int'];
     description: Scalars['String'];
@@ -3479,11 +3397,12 @@ export type TestShippingMethodQuote = {
 };
 
 export type TestShippingMethodResult = {
-    __typename?: 'TestShippingMethodResult';
     eligible: Scalars['Boolean'];
     quote?: Maybe<TestShippingMethodQuote>;
 };
 
+export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
+
 export type UpdateAddressInput = {
     id: Scalars['ID'];
     fullName?: Maybe<Scalars['String']>;
@@ -3701,7 +3620,6 @@ export type UpdateZoneInput = {
 };
 
 export type User = Node & {
-    __typename?: 'User';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3714,7 +3632,6 @@ export type User = Node & {
 };
 
 export type Zone = Node & {
-    __typename?: 'Zone';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3726,12 +3643,11 @@ export type CreateAssetsMutationVariables = Exact<{
     input: Array<CreateAssetInput>;
 }>;
 
-export type CreateAssetsMutation = { __typename?: 'Mutation' } & {
+export type CreateAssetsMutation = {
     createAssets: Array<
-        | ({ __typename?: 'Asset' } & Pick<Asset, 'id' | 'name' | 'source' | 'preview'> & {
-                  focalPoint?: Maybe<{ __typename?: 'Coordinate' } & Pick<Coordinate, 'x' | 'y'>>;
-              })
-        | { __typename?: 'MimeTypeError' }
+        Pick<Asset, 'id' | 'name' | 'source' | 'preview'> & {
+            focalPoint?: Maybe<Pick<Coordinate, 'x' | 'y'>>;
+        }
     >;
 };
 
@@ -3740,9 +3656,7 @@ export type DeleteAssetMutationVariables = Exact<{
     force: Scalars['Boolean'];
 }>;
 
-export type DeleteAssetMutation = { __typename?: 'Mutation' } & {
-    deleteAsset: { __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'result'>;
-};
+export type DeleteAssetMutation = { deleteAsset: Pick<DeletionResponse, 'result'> };
 
 type DiscriminateUnion<T, U> = T extends U ? T : never;
 

+ 299 - 27
packages/common/src/generated-shop-types.ts

@@ -16,6 +16,13 @@ export type Scalars = {
     Upload: any;
 };
 
+export type AddPaymentToOrderResult =
+    | Order
+    | OrderPaymentStateError
+    | PaymentFailedError
+    | PaymentDeclinedError
+    | OrderStateTransitionError;
+
 export type Address = Node & {
     __typename?: 'Address';
     id: Scalars['ID'];
@@ -70,6 +77,19 @@ export type AdministratorList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
+/** Retured when attemting to set the Customer for an Order when already logged in. */
+export type AlreadyLoggedInError = ErrorResult & {
+    __typename?: 'AlreadyLoggedInError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+export type ApplyCouponCodeResult =
+    | Order
+    | CouponCodeExpiredError
+    | CouponCodeInvalidError
+    | CouponCodeLimitError;
+
 export type Asset = Node & {
     __typename?: 'Asset';
     id: Scalars['ID'];
@@ -299,6 +319,31 @@ export type CountryTranslation = {
     name: Scalars['String'];
 };
 
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeExpiredError = ErrorResult & {
+    __typename?: 'CouponCodeExpiredError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeInvalidError = ErrorResult & {
+    __typename?: 'CouponCodeInvalidError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+};
+
+/** Returned if the provided coupon code is invalid */
+export type CouponCodeLimitError = ErrorResult & {
+    __typename?: 'CouponCodeLimitError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    couponCode: Scalars['String'];
+    limit: Scalars['Int'];
+};
+
 export type CreateAddressInput = {
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
@@ -807,10 +852,44 @@ export enum DeletionResult {
     NOT_DELETED = 'NOT_DELETED',
 }
 
+/** Retured when attemting to create a Customer with an email address already registered to an existing User. */
+export type EmailAddressConflictError = ErrorResult & {
+    __typename?: 'EmailAddressConflictError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export enum ErrorCode {
-    UnknownError = 'UnknownError',
+    UNKNOWN_ERROR = 'UNKNOWN_ERROR',
+    ORDER_MODIFICATION_ERROR = 'ORDER_MODIFICATION_ERROR',
+    ORDER_LIMIT_ERROR = 'ORDER_LIMIT_ERROR',
+    NEGATIVE_QUANTITY_ERROR = 'NEGATIVE_QUANTITY_ERROR',
+    COUPON_CODE_EXPIRED_ERROR = 'COUPON_CODE_EXPIRED_ERROR',
+    COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR',
+    COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR',
+    ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
+    ORDER_PAYMENT_STATE_ERROR = 'ORDER_PAYMENT_STATE_ERROR',
+    PAYMENT_FAILED_ERROR = 'PAYMENT_FAILED_ERROR',
+    PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
+    ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
+    EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+    MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+    VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
+    VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
+    PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_ERROR',
+    INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
+    IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
+    IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
+    PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
+    PASSWORD_RESET_TOKEN_EXPIRED_ERROR = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR',
 }
 
+export type ErrorResult = {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Facet = Node & {
     __typename?: 'Facet';
     id: Scalars['ID'];
@@ -970,6 +1049,26 @@ export enum HistoryEntryType {
     ORDER_COUPON_REMOVED = 'ORDER_COUPON_REMOVED',
 }
 
+/**
+ * Retured if the token used to change a Customer's email address is valid, but has
+ * expired according to the `verificationTokenDuration` setting in the AuthOptions.
+ */
+export type IdentifierChangeTokenExpiredError = ErrorResult & {
+    __typename?: 'IdentifierChangeTokenExpiredError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/**
+ * Retured if the token used to change a Customer's email address is either
+ * invalid or does not match any expected tokens.
+ */
+export type IdentifierChangeTokenInvalidError = ErrorResult & {
+    __typename?: 'IdentifierChangeTokenInvalidError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type ImportInfo = {
     __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
@@ -991,6 +1090,13 @@ export type IntCustomFieldConfig = CustomField & {
     step?: Maybe<Scalars['Int']>;
 };
 
+/** Returned if the user credentials are not valid */
+export type InvalidCredentialsError = ErrorResult & {
+    __typename?: 'InvalidCredentialsError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 /**
  * @description
  * Languages in the form of a ISO 639-1 language code with optional
@@ -1346,28 +1452,35 @@ export type LoginResult = {
     user: CurrentUser;
 };
 
+/** Retured when attemting to register or verify a customer account without a password, when one is required. */
+export type MissingPasswordError = ErrorResult & {
+    __typename?: 'MissingPasswordError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Mutation = {
     __typename?: 'Mutation';
     /**
      * Adds an item to the order. If custom fields are defined on the OrderLine
      * entity, a third argument 'customFields' will be available.
      */
-    addItemToOrder?: Maybe<Order>;
+    addItemToOrder: UpdateOrderItemsResult;
     /** Remove an OrderLine from the Order */
-    removeOrderLine?: Maybe<Order>;
+    removeOrderLine: RemoveOrderItemsResult;
     /** Remove all OrderLine from the Order */
-    removeAllOrderLines?: Maybe<Order>;
+    removeAllOrderLines: RemoveOrderItemsResult;
     /**
      * Adjusts an OrderLine. If custom fields are defined on the OrderLine entity, a
      * third argument 'customFields' of type `OrderLineCustomFieldsInput` will be available.
      */
-    adjustOrderLine?: Maybe<Order>;
+    adjustOrderLine: UpdateOrderItemsResult;
     /** Applies the given coupon code to the active Order */
-    applyCouponCode?: Maybe<Order>;
+    applyCouponCode: ApplyCouponCodeResult;
     /** Removes the given coupon code from the active Order */
     removeCouponCode?: Maybe<Order>;
     /** Transitions an Order to a new state. Valid next states can be found by querying `nextOrderStates` */
-    transitionOrderToState?: Maybe<Order>;
+    transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     /** Sets the shipping address for this order */
     setOrderShippingAddress?: Maybe<Order>;
     /** Sets the billing address for this order */
@@ -1375,11 +1488,11 @@ export type Mutation = {
     /** Allows any custom fields to be set for the active order */
     setOrderCustomFields?: Maybe<Order>;
     /** Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query */
-    setOrderShippingMethod?: Maybe<Order>;
+    setOrderShippingMethod: SetOrderShippingMethodResult;
     /** Add a Payment to the Order */
-    addPaymentToOrder?: Maybe<Order>;
+    addPaymentToOrder?: Maybe<AddPaymentToOrderResult>;
     /** Set the Customer for the Order. Required only if the Customer is not currently logged in */
-    setCustomerForOrder?: Maybe<Order>;
+    setCustomerForOrder?: Maybe<SetCustomerForOrderResult>;
     /**
      * Authenticates the user using the native authentication strategy. This mutation
      * is an alias for `authenticate({ native: { ... }})`
@@ -1389,11 +1502,6 @@ export type Mutation = {
     authenticate: LoginResult;
     /** End the current authenticated session */
     logout: Scalars['Boolean'];
-    /**
-     * Regenerate and send a verification token for a new Customer registration. Only
-     * applicable if `authOptions.requireVerification` is set to true.
-     */
-    refreshCustomerVerification: Scalars['Boolean'];
     /**
      * Register a Customer account with the given credentials. There are three possible registration flows:
      *
@@ -1415,7 +1523,12 @@ export type Mutation = {
      * 3. The Customer _must_ be registered _with_ a password. No further action is
      * needed - the Customer is able to authenticate immediately.
      */
-    registerCustomerAccount: Scalars['Boolean'];
+    registerCustomerAccount: RegisterCustomerAccountResult;
+    /**
+     * Regenerate and send a verification token for a new Customer registration. Only
+     * applicable if `authOptions.requireVerification` is set to true.
+     */
+    refreshCustomerVerification: RefreshCustomerVerificationResult;
     /** Update an existing Customer */
     updateCustomer: Customer;
     /** Create a new Customer Address */
@@ -1423,7 +1536,7 @@ export type Mutation = {
     /** Update an existing Address */
     updateCustomerAddress: Address;
     /** Delete an existing Address */
-    deleteCustomerAddress: Scalars['Boolean'];
+    deleteCustomerAddress: Success;
     /**
      * Verify a Customer email address with the token sent to that address. Only
      * applicable if `authOptions.requireVerification` is set to true.
@@ -1431,25 +1544,25 @@ export type Mutation = {
      * If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the a password _must_ be
      * provided here.
      */
-    verifyCustomerAccount: LoginResult;
+    verifyCustomerAccount: VerifyCustomerAccountResult;
     /** Update the password of the active Customer */
-    updateCustomerPassword?: Maybe<Scalars['Boolean']>;
+    updateCustomerPassword: UpdateCustomerPasswordResult;
     /**
      * Request to update the emailAddress of the active Customer. If `authOptions.requireVerification` is enabled
      * (as is the default), then the `identifierChangeToken` will be assigned to the current User and
      * a IdentifierChangeRequestEvent will be raised. This can then be used e.g. by the EmailPlugin to email
      * that verification token to the Customer, which is then used to verify the change of email address.
      */
-    requestUpdateCustomerEmailAddress?: Maybe<Scalars['Boolean']>;
+    requestUpdateCustomerEmailAddress: RequestUpdateCustomerEmailAddressResult;
     /**
      * Confirm the update of the emailAddress with the provided token, which has been generated by the
      * `requestUpdateCustomerEmailAddress` mutation.
      */
-    updateCustomerEmailAddress?: Maybe<Scalars['Boolean']>;
+    updateCustomerEmailAddress: UpdateCustomerEmailAddressResult;
     /** Requests a password reset email to be sent */
-    requestPasswordReset?: Maybe<Scalars['Boolean']>;
+    requestPasswordReset?: Maybe<RequestPasswordResetResult>;
     /** Resets a Customer's password based on the provided token */
-    resetPassword: LoginResult;
+    resetPassword: ResetPasswordResult;
 };
 
 export type MutationAddItemToOrderArgs = {
@@ -1513,14 +1626,14 @@ export type MutationAuthenticateArgs = {
     rememberMe?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationRefreshCustomerVerificationArgs = {
-    emailAddress: Scalars['String'];
-};
-
 export type MutationRegisterCustomerAccountArgs = {
     input: RegisterCustomerInput;
 };
 
+export type MutationRefreshCustomerVerificationArgs = {
+    emailAddress: Scalars['String'];
+};
+
 export type MutationUpdateCustomerArgs = {
     input: UpdateCustomerInput;
 };
@@ -1570,6 +1683,20 @@ export type NativeAuthInput = {
     password: Scalars['String'];
 };
 
+/** Retured when attempting an operation that relies on the NativeAuthStrategy, if that strategy is not configured. */
+export type NativeAuthStrategyError = ErrorResult & {
+    __typename?: 'NativeAuthStrategyError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Retured when attemting to set a negative OrderLine quantity. */
+export type NegativeQuantityError = ErrorResult & {
+    __typename?: 'NegativeQuantityError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Node = {
     id: Scalars['ID'];
 };
@@ -1670,6 +1797,14 @@ export type OrderItem = Node & {
     refundId?: Maybe<Scalars['ID']>;
 };
 
+/** Retured when the maximum order size limit has been reached. */
+export type OrderLimitError = ErrorResult & {
+    __typename?: 'OrderLimitError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    maxItems: Scalars['Int'];
+};
+
 export type OrderLine = Node & {
     __typename?: 'OrderLine';
     id: Scalars['ID'];
@@ -1700,6 +1835,20 @@ export type OrderListOptions = {
     filter?: Maybe<OrderFilterParameter>;
 };
 
+/** Returned when attempting to modify the contents of an Order that is not in the `AddingItems` state. */
+export type OrderModificationError = ErrorResult & {
+    __typename?: 'OrderModificationError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned when attempting to add a Payment to an Order that is not in the `ArrangingPayment` state. */
+export type OrderPaymentStateError = ErrorResult & {
+    __typename?: 'OrderPaymentStateError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type OrderProcessState = {
     __typename?: 'OrderProcessState';
     name: Scalars['String'];
@@ -1720,11 +1869,48 @@ export type OrderSortParameter = {
     total?: Maybe<SortOrder>;
 };
 
+/** Returned if there is an error in transitioning the Order state */
+export type OrderStateTransitionError = ErrorResult & {
+    __typename?: 'OrderStateTransitionError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 export type PaginatedList = {
     items: Array<Node>;
     totalItems: Scalars['Int'];
 };
 
+/** Retured when attemting to verify a customer account with a password, when a password has already been set. */
+export type PasswordAlreadySetError = ErrorResult & {
+    __typename?: 'PasswordAlreadySetError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/**
+ * Retured if the token used to reset a Customer's password is valid, but has
+ * expired according to the `verificationTokenDuration` setting in the AuthOptions.
+ */
+export type PasswordResetTokenExpiredError = ErrorResult & {
+    __typename?: 'PasswordResetTokenExpiredError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/**
+ * Retured if the token used to reset a Customer's password is either
+ * invalid or does not match any expected tokens.
+ */
+export type PasswordResetTokenInvalidError = ErrorResult & {
+    __typename?: 'PasswordResetTokenInvalidError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Payment = Node & {
     __typename?: 'Payment';
     id: Scalars['ID'];
@@ -1739,6 +1925,22 @@ export type Payment = Node & {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+/** Returned when a Payment is declined by the payment provider. */
+export type PaymentDeclinedError = ErrorResult & {
+    __typename?: 'PaymentDeclinedError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    paymentErrorMessage: Scalars['String'];
+};
+
+/** Returned when a Payment fails due to an error. */
+export type PaymentFailedError = ErrorResult & {
+    __typename?: 'PaymentFailedError';
+    code: ErrorCode;
+    message: Scalars['String'];
+    paymentErrorMessage: Scalars['String'];
+};
+
 /** Passed as input to the `addPaymentToOrder` mutation. */
 export type PaymentInput = {
     /** This field should correspond to the `code` property of a PaymentMethodHandler. */
@@ -2082,6 +2284,8 @@ export type QuerySearchArgs = {
     input: SearchInput;
 };
 
+export type RefreshCustomerVerificationResult = Success | NativeAuthStrategyError;
+
 export type Refund = Node & {
     __typename?: 'Refund';
     id: Scalars['ID'];
@@ -2100,6 +2304,8 @@ export type Refund = Node & {
     metadata?: Maybe<Scalars['JSON']>;
 };
 
+export type RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError;
+
 export type RegisterCustomerInput = {
     emailAddress: Scalars['String'];
     title?: Maybe<Scalars['String']>;
@@ -2109,6 +2315,22 @@ export type RegisterCustomerInput = {
     password?: Maybe<Scalars['String']>;
 };
 
+export type RemoveOrderItemsResult = Order | OrderModificationError;
+
+export type RequestPasswordResetResult = Success | NativeAuthStrategyError;
+
+export type RequestUpdateCustomerEmailAddressResult =
+    | Success
+    | InvalidCredentialsError
+    | EmailAddressConflictError
+    | NativeAuthStrategyError;
+
+export type ResetPasswordResult =
+    | CurrentUser
+    | PasswordResetTokenInvalidError
+    | PasswordResetTokenExpiredError
+    | NativeAuthStrategyError;
+
 export type Return = Node &
     StockMovement & {
         __typename?: 'Return';
@@ -2222,6 +2444,10 @@ export type ServerConfig = {
     customFieldConfig: CustomFields;
 };
 
+export type SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError;
+
+export type SetOrderShippingMethodResult = Order | OrderModificationError;
+
 export type ShippingMethod = Node & {
     __typename?: 'ShippingMethod';
     id: Scalars['ID'];
@@ -2320,6 +2546,12 @@ export type StringOperators = {
     contains?: Maybe<Scalars['String']>;
 };
 
+/** Indicates that an operation succeeded, where we do not want to return any more specific information. */
+export type Success = {
+    __typename?: 'Success';
+    success: Scalars['Boolean'];
+};
+
 export type TaxCategory = Node & {
     __typename?: 'TaxCategory';
     id: Scalars['ID'];
@@ -2347,6 +2579,8 @@ export type TaxRateList = PaginatedList & {
     totalItems: Scalars['Int'];
 };
 
+export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
+
 export type UpdateAddressInput = {
     id: Scalars['ID'];
     fullName?: Maybe<Scalars['String']>;
@@ -2363,6 +2597,12 @@ export type UpdateAddressInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type UpdateCustomerEmailAddressResult =
+    | Success
+    | IdentifierChangeTokenInvalidError
+    | IdentifierChangeTokenExpiredError
+    | NativeAuthStrategyError;
+
 export type UpdateCustomerInput = {
     title?: Maybe<Scalars['String']>;
     firstName?: Maybe<Scalars['String']>;
@@ -2371,10 +2611,14 @@ export type UpdateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type UpdateOrderInput = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError;
+
 export type User = Node & {
     __typename?: 'User';
     id: Scalars['ID'];
@@ -2388,6 +2632,34 @@ export type User = Node & {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+/**
+ * Returned if the verification token (used to verify a Customer's email address) is valid, but has
+ * expired according to the `verificationTokenDuration` setting in the AuthOptions.
+ */
+export type VerificationTokenExpiredError = ErrorResult & {
+    __typename?: 'VerificationTokenExpiredError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/**
+ * Retured if the verification token (used to verify a Customer's email address) is either
+ * invalid or does not match any expected tokens.
+ */
+export type VerificationTokenInvalidError = ErrorResult & {
+    __typename?: 'VerificationTokenInvalidError';
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+export type VerifyCustomerAccountResult =
+    | CurrentUser
+    | VerificationTokenInvalidError
+    | VerificationTokenExpiredError
+    | MissingPasswordError
+    | PasswordAlreadySetError
+    | NativeAuthStrategyError;
+
 export type Zone = Node & {
     __typename?: 'Zone';
     id: Scalars['ID'];

+ 23 - 4
packages/common/src/generated-types.ts

@@ -1123,8 +1123,9 @@ export enum DeletionResult {
 }
 
 export enum ErrorCode {
-  UnknownError = 'UnknownError',
-  MimeTypeError = 'MimeTypeError'
+  UNKNOWN_ERROR = 'UNKNOWN_ERROR',
+  MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
+  ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR'
 }
 
 export type ErrorResult = {
@@ -1865,7 +1866,7 @@ export type Mutation = {
   /** Update an existing Address */
   updateCustomerAddress: Address;
   /** Update an existing Address */
-  deleteCustomerAddress: Scalars['Boolean'];
+  deleteCustomerAddress: Success;
   addNoteToCustomer: Customer;
   updateCustomerNote: HistoryEntry;
   deleteCustomerNote: DeletionResponse;
@@ -1893,7 +1894,7 @@ export type Mutation = {
   addNoteToOrder: Order;
   updateOrderNote: HistoryEntry;
   deleteOrderNote: DeletionResponse;
-  transitionOrderToState?: Maybe<Order>;
+  transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
   transitionFulfillmentToState: Fulfillment;
   setOrderCustomFields?: Maybe<Order>;
   /** Update an existing PaymentMethod */
@@ -2592,6 +2593,16 @@ export type OrderSortParameter = {
   total?: Maybe<SortOrder>;
 };
 
+/** Returned if there is an error in transitioning the Order state */
+export type OrderStateTransitionError = ErrorResult & {
+  __typename?: 'OrderStateTransitionError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  transitionError: Scalars['String'];
+  fromState: Scalars['String'];
+  toState: Scalars['String'];
+};
+
 export type PaginatedList = {
   items: Array<Node>;
   totalItems: Scalars['Int'];
@@ -3530,6 +3541,12 @@ export type StringOperators = {
   contains?: Maybe<Scalars['String']>;
 };
 
+/** Indicates that an operation succeeded, where we do not want to return any more specific information. */
+export type Success = {
+  __typename?: 'Success';
+  success: Scalars['Boolean'];
+};
+
 export type TaxCategory = Node & {
   __typename?: 'TaxCategory';
   id: Scalars['ID'];
@@ -3611,6 +3628,8 @@ export type TestShippingMethodResult = {
   quote?: Maybe<TestShippingMethodQuote>;
 };
 
+export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
+
 export type UpdateAddressInput = {
   id: Scalars['ID'];
   fullName?: Maybe<Scalars['String']>;

+ 4 - 3
packages/core/e2e/asset.e2e-spec.ts

@@ -1,7 +1,7 @@
 /* tslint:disable:no-non-null-assertion */
 import { omit } from '@vendure/common/lib/omit';
 import { mergeConfig } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -10,6 +10,7 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf
 
 import { ASSET_FRAGMENT } from './graphql/fragments';
 import {
+    AssetFragment,
     CreateAssets,
     DeleteAsset,
     DeletionResult,
@@ -136,7 +137,7 @@ describe('Asset resolver', () => {
     });
 
     describe('createAssets', () => {
-        function isAsset(input: CreateAssets.CreateAssets): input is CreateAssets.AssetInlineFragment {
+        function isAsset(input: CreateAssets.CreateAssets): input is AssetFragment {
             return input.hasOwnProperty('name');
         }
 
@@ -216,7 +217,7 @@ describe('Asset resolver', () => {
 
             expect(createAssets.length).toBe(1);
             expect(createAssets[0]).toEqual({
-                message: 'error.mime-type-not-permitted',
+                message: `The MIME type 'text/plain' is not permitted.`,
                 mimeType: 'text/plain',
                 fileName: 'dummy.txt',
             });

+ 7 - 2
packages/core/e2e/authentication-strategy.e2e-spec.ts

@@ -1,6 +1,6 @@
 import { pick } from '@vendure/common/lib/pick';
 import { mergeConfig } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -160,6 +160,10 @@ describe('AuthenticationStrategy', () => {
         });
 
         it('registerCustomerAccount with external email', async () => {
+            const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard<{
+                success: boolean;
+            }>(input => input.success != null);
+
             const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
                 REGISTER_ACCOUNT,
                 {
@@ -168,8 +172,9 @@ describe('AuthenticationStrategy', () => {
                     },
                 },
             );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
 
-            expect(registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             const { customer } = await adminClient.query<
                 GetCustomerUserAuth.Query,
                 GetCustomerUserAuth.Variables

+ 11 - 4
packages/core/e2e/customer.e2e-spec.ts

@@ -9,7 +9,7 @@ import {
     mergeConfig,
     VendurePlugin,
 } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -34,7 +34,7 @@ import {
     UpdateCustomer,
     UpdateCustomerNote,
 } from './graphql/generated-e2e-admin-types';
-import { AddItemToOrder } from './graphql/generated-e2e-shop-types';
+import { AddItemToOrder, UpdatedOrderFragment } from './graphql/generated-e2e-shop-types';
 import {
     CREATE_ADDRESS,
     CREATE_CUSTOMER,
@@ -331,13 +331,15 @@ describe('Customer resolver', () => {
             >(
                 gql`
                     mutation DeleteCustomerAddress($id: ID!) {
-                        deleteCustomerAddress(id: $id)
+                        deleteCustomerAddress(id: $id) {
+                            success
+                        }
                     }
                 `,
                 { id: firstCustomerThirdAddressId },
             );
 
-            expect(deleteCustomerAddress).toBe(true);
+            expect(deleteCustomerAddress.success).toBe(true);
 
             const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
                 GET_CUSTOMER,
@@ -358,6 +360,10 @@ describe('Customer resolver', () => {
     });
 
     describe('orders', () => {
+        const orderResultGuard: ErrorResultGuard<UpdatedOrderFragment> = createErrorResultGuard<
+            UpdatedOrderFragment
+        >(input => !!input.lines);
+
         it(`lists that user\'s orders`, async () => {
             // log in as first customer
             await shopClient.asUserWithCredentials(firstCustomer.emailAddress, 'test');
@@ -369,6 +375,7 @@ describe('Customer resolver', () => {
                 productVariantId: 'T_1',
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
 
             const { customer } = await adminClient.query<
                 GetCustomerOrders.Query,

+ 4 - 3
packages/core/e2e/fixtures/test-payment-methods.ts

@@ -14,7 +14,7 @@ export const testSuccessfulPaymentMethod = new PaymentMethodHandler({
             metadata,
         };
     },
-    settlePayment: (order) => ({
+    settlePayment: order => ({
         success: true,
     }),
 });
@@ -105,10 +105,11 @@ export const testFailingPaymentMethod = new PaymentMethodHandler({
         return {
             amount: order.total,
             state: 'Declined',
+            errorMessage: 'Insufficient funds',
             metadata,
         };
     },
-    settlePayment: (order) => ({
+    settlePayment: order => ({
         success: true,
     }),
 });
@@ -124,7 +125,7 @@ export const testErrorPaymentMethod = new PaymentMethodHandler({
             metadata,
         };
     },
-    settlePayment: (order) => ({
+    settlePayment: order => ({
         success: true,
     }),
 });

+ 2 - 0
packages/core/e2e/graphql/fragments.ts

@@ -452,6 +452,7 @@ export const TAX_RATE_FRAGMENT = gql`
         }
     }
 `;
+
 export const CURRENT_USER_FRAGMENT = gql`
     fragment CurrentUser on CurrentUser {
         id
@@ -463,6 +464,7 @@ export const CURRENT_USER_FRAGMENT = gql`
         }
     }
 `;
+
 export const VARIANT_WITH_STOCK_FRAGMENT = gql`
     fragment VariantWithStock on ProductVariant {
         id

ファイルの差分が大きいため隠しています
+ 57 - 186
packages/core/e2e/graphql/generated-e2e-admin-types.ts


ファイルの差分が大きいため隠しています
+ 233 - 109
packages/core/e2e/graphql/generated-e2e-shop-types.ts


+ 9 - 3
packages/core/e2e/graphql/shared-definitions.ts

@@ -641,9 +641,15 @@ export const GET_CUSTOMER_WITH_GROUPS = gql`
 export const ADMIN_TRANSITION_TO_STATE = gql`
     mutation AdminTransition($id: ID!, $state: String!) {
         transitionOrderToState(id: $id, state: $state) {
-            id
-            state
-            nextStates
+            ...Order
+            ... on OrderStateTransitionError {
+                errorCode: code
+                message
+                transitionError
+                fromState
+                toState
+            }
         }
     }
+    ${ORDER_FRAGMENT}
 `;

+ 195 - 53
packages/core/e2e/graphql/shop-definitions.ts

@@ -50,26 +50,18 @@ export const TEST_ORDER_FRAGMENT = gql`
     }
 `;
 
-export const ADD_ITEM_TO_ORDER = gql`
-    mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
-        addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
+export const UPDATED_ORDER_FRAGMENT = gql`
+    fragment UpdatedOrder on Order {
+        id
+        code
+        state
+        active
+        total
+        lines {
             id
-            code
-            state
-            active
-            total
-            lines {
+            quantity
+            productVariant {
                 id
-                quantity
-                productVariant {
-                    id
-                }
-                adjustments {
-                    adjustmentSource
-                    amount
-                    description
-                    type
-                }
             }
             adjustments {
                 adjustmentSource
@@ -78,7 +70,26 @@ export const ADD_ITEM_TO_ORDER = gql`
                 type
             }
         }
+        adjustments {
+            adjustmentSource
+            amount
+            description
+            type
+        }
+    }
+`;
+
+export const ADD_ITEM_TO_ORDER = gql`
+    mutation AddItemToOrder($productVariantId: ID!, $quantity: Int!) {
+        addItemToOrder(productVariantId: $productVariantId, quantity: $quantity) {
+            ...UpdatedOrder
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
+        }
     }
+    ${UPDATED_ORDER_FRAGMENT}
 `;
 export const SEARCH_PRODUCTS_SHOP = gql`
     query SearchProductsShop($input: SearchInput!) {
@@ -108,47 +119,105 @@ export const SEARCH_PRODUCTS_SHOP = gql`
 `;
 export const REGISTER_ACCOUNT = gql`
     mutation Register($input: RegisterCustomerInput!) {
-        registerCustomerAccount(input: $input)
+        registerCustomerAccount(input: $input) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
+    }
+`;
+
+export const CURRENT_USER_FRAGMENT = gql`
+    fragment CurrentUserShop on CurrentUser {
+        id
+        identifier
+        channels {
+            code
+            token
+            permissions
+        }
     }
 `;
+
 export const VERIFY_EMAIL = gql`
     mutation Verify($password: String, $token: String!) {
         verifyCustomerAccount(password: $password, token: $token) {
-            user {
-                id
-                identifier
+            ...CurrentUserShop
+            ... on ErrorResult {
+                code
+                message
             }
         }
     }
+    ${CURRENT_USER_FRAGMENT}
 `;
+
 export const REFRESH_TOKEN = gql`
     mutation RefreshToken($emailAddress: String!) {
-        refreshCustomerVerification(emailAddress: $emailAddress)
+        refreshCustomerVerification(emailAddress: $emailAddress) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
     }
 `;
 export const REQUEST_PASSWORD_RESET = gql`
     mutation RequestPasswordReset($identifier: String!) {
-        requestPasswordReset(emailAddress: $identifier)
+        requestPasswordReset(emailAddress: $identifier) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
     }
 `;
 export const RESET_PASSWORD = gql`
     mutation ResetPassword($token: String!, $password: String!) {
         resetPassword(token: $token, password: $password) {
-            user {
-                id
-                identifier
+            ...CurrentUserShop
+            ... on ErrorResult {
+                code
+                message
             }
         }
     }
+    ${CURRENT_USER_FRAGMENT}
 `;
 export const REQUEST_UPDATE_EMAIL_ADDRESS = gql`
     mutation RequestUpdateEmailAddress($password: String!, $newEmailAddress: String!) {
-        requestUpdateCustomerEmailAddress(password: $password, newEmailAddress: $newEmailAddress)
+        requestUpdateCustomerEmailAddress(password: $password, newEmailAddress: $newEmailAddress) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
     }
 `;
 export const UPDATE_EMAIL_ADDRESS = gql`
     mutation UpdateEmailAddress($token: String!) {
-        updateCustomerEmailAddress(token: $token)
+        updateCustomerEmailAddress(token: $token) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
     }
 `;
 export const GET_ACTIVE_CUSTOMER = gql`
@@ -182,7 +251,9 @@ export const UPDATE_ADDRESS = gql`
 `;
 export const DELETE_ADDRESS = gql`
     mutation DeleteAddressShop($id: ID!) {
-        deleteCustomerAddress(id: $id)
+        deleteCustomerAddress(id: $id) {
+            success
+        }
     }
 `;
 
@@ -198,7 +269,15 @@ export const UPDATE_CUSTOMER = gql`
 
 export const UPDATE_PASSWORD = gql`
     mutation UpdatePassword($old: String!, $new: String!) {
-        updateCustomerPassword(currentPassword: $old, newPassword: $new)
+        updateCustomerPassword(currentPassword: $old, newPassword: $new) {
+            ... on Success {
+                success
+            }
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
     }
 `;
 
@@ -215,6 +294,10 @@ export const ADJUST_ITEM_QUANTITY = gql`
     mutation AdjustItemQuantity($orderLineId: ID!, $quantity: Int!) {
         adjustOrderLine(orderLineId: $orderLineId, quantity: $quantity) {
             ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}
@@ -224,6 +307,10 @@ export const REMOVE_ITEM_FROM_ORDER = gql`
     mutation RemoveItemFromOrder($orderLineId: ID!) {
         removeOrderLine(orderLineId: $orderLineId) {
             ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}
@@ -242,28 +329,42 @@ export const GET_ELIGIBLE_SHIPPING_METHODS = gql`
 export const SET_SHIPPING_METHOD = gql`
     mutation SetShippingMethod($id: ID!) {
         setOrderShippingMethod(shippingMethodId: $id) {
-            shipping
-            shippingMethod {
-                id
-                code
-                description
+            ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode: code
+                message
             }
         }
     }
+    ${TEST_ORDER_FRAGMENT}
+`;
+
+export const ACTIVE_ORDER_CUSTOMER = gql`
+    fragment ActiveOrderCustomer on Order {
+        id
+        customer {
+            id
+            emailAddress
+            firstName
+            lastName
+        }
+        lines {
+            id
+        }
+    }
 `;
 
 export const SET_CUSTOMER = gql`
     mutation SetCustomerForOrder($input: CreateCustomerInput!) {
         setCustomerForOrder(input: $input) {
-            id
-            customer {
-                id
-                emailAddress
-                firstName
-                lastName
+            ...ActiveOrderCustomer
+            ... on ErrorResult {
+                errorCode: code
+                message
             }
         }
     }
+    ${ACTIVE_ORDER_CUSTOMER}
 `;
 
 export const GET_ORDER_BY_CODE = gql`
@@ -300,10 +401,17 @@ export const GET_AVAILABLE_COUNTRIES = gql`
 export const TRANSITION_TO_STATE = gql`
     mutation TransitionToState($state: String!) {
         transitionOrderToState(state: $state) {
-            id
-            state
+            ...TestOrderFragment
+            ... on OrderStateTransitionError {
+                errorCode: code
+                message
+                transitionError
+                fromState
+                toState
+            }
         }
     }
+    ${TEST_ORDER_FRAGMENT}
 `;
 
 export const SET_SHIPPING_ADDRESS = gql`
@@ -342,21 +450,47 @@ export const SET_BILLING_ADDRESS = gql`
     }
 `;
 
+export const TEST_ORDER_WITH_PAYMENTS_FRAGMENT = gql`
+    fragment TestOrderWithPayments on Order {
+        ...TestOrderFragment
+        payments {
+            id
+            transactionId
+            method
+            amount
+            state
+            metadata
+        }
+    }
+    ${TEST_ORDER_FRAGMENT}
+`;
+
+export const GET_ACTIVE_ORDER_WITH_PAYMENTS = gql`
+    query GetActiveOrderWithPayments {
+        activeOrder {
+            ...TestOrderWithPayments
+        }
+    }
+    ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT}
+`;
+
 export const ADD_PAYMENT = gql`
     mutation AddPaymentToOrder($input: PaymentInput!) {
         addPaymentToOrder(input: $input) {
-            ...TestOrderFragment
-            payments {
-                id
-                transactionId
-                method
-                amount
-                state
-                metadata
+            ...TestOrderWithPayments
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
+            ... on PaymentDeclinedError {
+                paymentErrorMessage
+            }
+            ... on PaymentFailedError {
+                paymentErrorMessage
             }
         }
     }
-    ${TEST_ORDER_FRAGMENT}
+    ${TEST_ORDER_WITH_PAYMENTS_FRAGMENT}
 `;
 
 export const GET_ACTIVE_ORDER_PAYMENTS = gql`
@@ -413,6 +547,10 @@ export const APPLY_COUPON_CODE = gql`
     mutation ApplyCouponCode($couponCode: String!) {
         applyCouponCode(couponCode: $couponCode) {
             ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}
@@ -431,6 +569,10 @@ export const REMOVE_ALL_ORDER_LINES = gql`
     mutation RemoveAllOrderLines {
         removeAllOrderLines {
             ...TestOrderFragment
+            ... on ErrorResult {
+                errorCode: code
+                message
+            }
         }
     }
     ${TEST_ORDER_FRAGMENT}

+ 13 - 2
packages/core/e2e/order-channel.e2e-spec.ts

@@ -1,5 +1,10 @@
 /* tslint:disable:no-non-null-assertion */
-import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    E2E_DEFAULT_CHANNEL_TOKEN,
+    ErrorResultGuard,
+} from '@vendure/testing';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
@@ -15,7 +20,7 @@ import {
     GetProductWithVariants,
     LanguageCode,
 } from './graphql/generated-e2e-admin-types';
-import { AddItemToOrder, GetActiveOrder } from './graphql/generated-e2e-shop-types';
+import { AddItemToOrder, GetActiveOrder, UpdatedOrderFragment } from './graphql/generated-e2e-shop-types';
 import {
     ASSIGN_PRODUCT_TO_CHANNEL,
     CREATE_CHANNEL,
@@ -120,6 +125,10 @@ describe('Channelaware orders', () => {
         await server.destroy();
     });
 
+    const orderResultGuard: ErrorResultGuard<UpdatedOrderFragment> = createErrorResultGuard<
+        UpdatedOrderFragment
+    >(input => !!input.lines);
+
     it('creates order on current channel', async () => {
         shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
         const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
@@ -129,6 +138,7 @@ describe('Channelaware orders', () => {
                 quantity: 1,
             },
         );
+        orderResultGuard.assertSuccess(addItemToOrder);
 
         expect(addItemToOrder!.lines.length).toBe(1);
         expect(addItemToOrder!.lines[0].quantity).toBe(1);
@@ -151,6 +161,7 @@ describe('Channelaware orders', () => {
                 quantity: 1,
             },
         );
+        orderResultGuard.assertSuccess(addItemToOrder);
 
         expect(addItemToOrder!.lines.length).toBe(1);
         expect(addItemToOrder!.lines[0].quantity).toBe(1);

+ 114 - 91
packages/core/e2e/order-process.e2e-spec.ts

@@ -1,20 +1,22 @@
 /* tslint:disable:no-non-null-assertion */
 import { CustomOrderProcess, mergeConfig, OrderState } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
-import { AdminTransition, GetOrder } from './graphql/generated-e2e-admin-types';
+import { AdminTransition, GetOrder, OrderFragment } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrder,
     AddPaymentToOrder,
+    ErrorCode,
     GetNextOrderStates,
     SetCustomerForOrder,
     SetShippingAddress,
     SetShippingMethod,
+    TestOrderFragmentFragment,
     TransitionToState,
 } from './graphql/generated-e2e-shop-types';
 import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
@@ -81,6 +83,10 @@ describe('Order process', () => {
         },
     };
 
+    const orderErrorGuard: ErrorResultGuard<
+        TestOrderFragmentFragment | OrderFragment
+    > = createErrorResultGuard<TestOrderFragmentFragment | OrderFragment>(input => !!input.total);
+
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
             orderOptions: { process: [customOrderProcess as any, customOrderProcess2 as any] },
@@ -130,6 +136,7 @@ describe('Order process', () => {
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
+            orderErrorGuard.assertSuccess(transitionOrderToState);
 
             expect(transitionStartSpy).toHaveBeenCalledTimes(1);
             expect(transitionEndSpy).not.toHaveBeenCalled();
@@ -155,17 +162,21 @@ describe('Order process', () => {
                 },
             );
 
-            try {
-                const { transitionOrderToState } = await shopClient.query<
-                    TransitionToState.Mutation,
-                    TransitionToState.Variables
-                >(TRANSITION_TO_STATE, {
-                    state: 'ValidatingCustomer',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(VALIDATION_ERROR_MESSAGE);
-            }
+            const { transitionOrderToState } = await shopClient.query<
+                TransitionToState.Mutation,
+                TransitionToState.Variables
+            >(TRANSITION_TO_STATE, {
+                state: 'ValidatingCustomer',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
+            );
+            expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
+            expect(transitionOrderToState!.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
+            expect(transitionOrderToState!.fromState).toBe('AddingItems');
+            expect(transitionOrderToState!.toState).toBe('ValidatingCustomer');
 
             expect(transitionStartSpy).toHaveBeenCalledTimes(1);
             expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
@@ -197,6 +208,7 @@ describe('Order process', () => {
             >(TRANSITION_TO_STATE, {
                 state: 'ValidatingCustomer',
             });
+            orderErrorGuard.assertSuccess(transitionOrderToState);
 
             expect(transitionEndSpy).toHaveBeenCalledTimes(1);
             expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
@@ -227,7 +239,7 @@ describe('Order process', () => {
     });
 
     describe('Admin API transition constraints', () => {
-        let order: NonNullable<TransitionToState.Mutation['transitionOrderToState']>;
+        let order: NonNullable<TestOrderFragmentFragment>;
 
         beforeAll(async () => {
             await shopClient.asAnonymousUser();
@@ -268,32 +280,35 @@ describe('Order process', () => {
                     state: 'ValidatingCustomer',
                 },
             );
-            const result = await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
-                TRANSITION_TO_STATE,
-                {
-                    state: 'ArrangingPayment',
-                },
-            );
-            order = result.transitionOrderToState!;
+            const { transitionOrderToState } = await shopClient.query<
+                TransitionToState.Mutation,
+                TransitionToState.Variables
+            >(TRANSITION_TO_STATE, {
+                state: 'ArrangingPayment',
+            });
+            orderErrorGuard.assertSuccess(transitionOrderToState);
+
+            order = transitionOrderToState!;
         });
 
         it('cannot manually transition to PaymentAuthorized', async () => {
             expect(order.state).toBe('ArrangingPayment');
 
-            try {
-                const { transitionOrderToState } = await adminClient.query<
-                    AdminTransition.Mutation,
-                    AdminTransition.Variables
-                >(ADMIN_TRANSITION_TO_STATE, {
-                    id: order.id,
-                    state: 'PaymentAuthorized',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
-                );
-            }
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'PaymentAuthorized',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"',
+            );
+            expect(transitionOrderToState!.transitionError).toBe(
+                'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
+            );
 
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
@@ -302,20 +317,21 @@ describe('Order process', () => {
         });
 
         it('cannot manually transition to PaymentSettled', async () => {
-            try {
-                const { transitionOrderToState } = await adminClient.query<
-                    AdminTransition.Mutation,
-                    AdminTransition.Variables
-                >(ADMIN_TRANSITION_TO_STATE, {
-                    id: order.id,
-                    state: 'PaymentSettled',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
-                );
-            }
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'PaymentSettled',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
+            );
+            expect(transitionOrderToState!.transitionError).toContain(
+                'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
+            );
 
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
@@ -333,23 +349,26 @@ describe('Order process', () => {
                     metadata: {},
                 },
             });
+            orderErrorGuard.assertSuccess(addPaymentToOrder);
 
             expect(addPaymentToOrder?.state).toBe('PaymentSettled');
 
-            try {
-                const { transitionOrderToState } = await adminClient.query<
-                    AdminTransition.Mutation,
-                    AdminTransition.Variables
-                >(ADMIN_TRANSITION_TO_STATE, {
-                    id: order.id,
-                    state: 'Cancelled',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
-                );
-            }
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'Cancelled',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "PaymentSettled" to "Cancelled"',
+            );
+            expect(transitionOrderToState!.transitionError).toContain(
+                'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
+            );
+
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
             });
@@ -357,20 +376,22 @@ describe('Order process', () => {
         });
 
         it('cannot manually transition to PartiallyDelivered', async () => {
-            try {
-                const { transitionOrderToState } = await adminClient.query<
-                    AdminTransition.Mutation,
-                    AdminTransition.Variables
-                >(ADMIN_TRANSITION_TO_STATE, {
-                    id: order.id,
-                    state: 'PartiallyDelivered',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
-                );
-            }
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'PartiallyDelivered',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
+            );
+            expect(transitionOrderToState!.transitionError).toContain(
+                'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
+            );
+
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
             });
@@ -378,20 +399,22 @@ describe('Order process', () => {
         });
 
         it('cannot manually transition to PartiallyDelivered', async () => {
-            try {
-                const { transitionOrderToState } = await adminClient.query<
-                    AdminTransition.Mutation,
-                    AdminTransition.Variables
-                >(ADMIN_TRANSITION_TO_STATE, {
-                    id: order.id,
-                    state: 'Delivered',
-                });
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
-                );
-            }
+            const { transitionOrderToState } = await adminClient.query<
+                AdminTransition.Mutation,
+                AdminTransition.Variables
+            >(ADMIN_TRANSITION_TO_STATE, {
+                id: order.id,
+                state: 'Delivered',
+            });
+            orderErrorGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
+                'Cannot transition Order from "PaymentSettled" to "Delivered"',
+            );
+            expect(transitionOrderToState!.transitionError).toContain(
+                'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
+            );
+
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
             });

+ 74 - 54
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -10,7 +10,7 @@ import {
     orderPercentageDiscount,
     productsPercentageDiscount,
 } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -32,11 +32,14 @@ import {
     AdjustItemQuantity,
     AdjustmentType,
     ApplyCouponCode,
+    ErrorCode,
     GetActiveOrder,
     GetOrderPromotionsByCode,
     RemoveCouponCode,
     SetCustomerForOrder,
     TestOrderFragment,
+    TestOrderFragmentFragment,
+    UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 import {
     CREATE_CUSTOMER_GROUP,
@@ -76,6 +79,11 @@ describe('Promotions applied to Orders', () => {
         ],
     });
 
+    type OrderSuccessResult = UpdatedOrderFragment | TestOrderFragmentFragment;
+    const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard<OrderSuccessResult>(
+        input => !!input.lines,
+    );
+
     let products: GetPromoProducts.Items[];
 
     beforeAll(async () => {
@@ -119,10 +127,7 @@ describe('Promotions applied to Orders', () => {
 
             await shopClient.asAnonymousUser();
             const item60 = getVariantBySlug('item-60');
-            const { addItemToOrder } = await shopClient.query<
-                AddItemToOrder.Mutation,
-                AddItemToOrder.Variables
-            >(ADD_ITEM_TO_ORDER, {
+            await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
                 productVariantId: item60.id,
                 quantity: 1,
             });
@@ -133,29 +138,29 @@ describe('Promotions applied to Orders', () => {
             await deletePromotion(promoFreeWithExpiredCoupon.id);
         });
 
-        it(
-            'applyCouponCode throws with nonexistant code',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
-                    APPLY_COUPON_CODE,
-                    {
-                        couponCode: 'bad code',
-                    },
-                );
-            }, 'Coupon code "bad code" is not valid'),
-        );
+        it('applyCouponCode returns error result when code is nonexistant', async () => {
+            const { applyCouponCode } = await shopClient.query<
+                ApplyCouponCode.Mutation,
+                ApplyCouponCode.Variables
+            >(APPLY_COUPON_CODE, {
+                couponCode: 'bad code',
+            });
+            orderResultGuard.assertErrorResult(applyCouponCode);
+            expect(applyCouponCode.message).toBe('Coupon code "bad code" is not valid');
+            expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_INVALID_ERROR);
+        });
 
-        it(
-            'applyCouponCode throws with expired code',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
-                    APPLY_COUPON_CODE,
-                    {
-                        couponCode: EXPIRED_COUPON_CODE,
-                    },
-                );
-            }, `Coupon code "${EXPIRED_COUPON_CODE}" has expired`),
-        );
+        it('applyCouponCode returns error when code is expired', async () => {
+            const { applyCouponCode } = await shopClient.query<
+                ApplyCouponCode.Mutation,
+                ApplyCouponCode.Variables
+            >(APPLY_COUPON_CODE, {
+                couponCode: EXPIRED_COUPON_CODE,
+            });
+            orderResultGuard.assertErrorResult(applyCouponCode);
+            expect(applyCouponCode.message).toBe(`Coupon code "${EXPIRED_COUPON_CODE}" has expired`);
+            expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_EXPIRED_ERROR);
+        });
 
         it('applies a valid coupon code', async () => {
             const { applyCouponCode } = await shopClient.query<
@@ -164,7 +169,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode: TEST_COUPON_CODE,
             });
-
+            orderResultGuard.assertSuccess(applyCouponCode);
             expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
             expect(applyCouponCode!.adjustments.length).toBe(1);
             expect(applyCouponCode!.adjustments[0].description).toBe('Free with test coupon');
@@ -192,7 +197,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode: TEST_COUPON_CODE,
             });
-
+            orderResultGuard.assertSuccess(applyCouponCode);
             expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
         });
 
@@ -274,6 +279,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: item60.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.total).toBe(6000);
             expect(addItemToOrder!.adjustments.length).toBe(0);
 
@@ -284,6 +290,7 @@ describe('Promotions applied to Orders', () => {
                 orderLineId: addItemToOrder!.lines[0].id,
                 quantity: 2,
             });
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.total).toBe(0);
             expect(adjustOrderLine!.adjustments[0].description).toBe('Free if order total greater than 100');
             expect(adjustOrderLine!.adjustments[0].amount).toBe(-12000);
@@ -318,6 +325,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: itemSale1.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(res1);
             expect(res1!.total).toBe(120);
             expect(res1!.adjustments.length).toBe(0);
 
@@ -328,6 +336,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: itemSale12.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(res2);
             expect(res2!.total).toBe(0);
             expect(res2!.adjustments.length).toBe(1);
             expect(res2!.total).toBe(0);
@@ -367,6 +376,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: item12.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.total).toBe(7200);
             expect(addItemToOrder!.adjustments.length).toBe(0);
 
@@ -377,6 +387,7 @@ describe('Promotions applied to Orders', () => {
                 orderLineId: addItemToOrder!.lines[0].id,
                 quantity: 2,
             });
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.total).toBe(0);
             expect(adjustOrderLine!.adjustments[0].description).toBe(
                 'Free if buying 3 or more offer products',
@@ -415,6 +426,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: getVariantBySlug('item-60').id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.total).toBe(0);
             expect(addItemToOrder!.adjustments.length).toBe(1);
             expect(addItemToOrder!.adjustments[0].description).toBe('Free for group members');
@@ -435,6 +447,7 @@ describe('Promotions applied to Orders', () => {
                 orderLineId: addItemToOrder!.lines[0].id,
                 quantity: 2,
             });
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.total).toBe(12000);
             expect(adjustOrderLine!.adjustments.length).toBe(0);
 
@@ -469,6 +482,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: item60.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.total).toBe(6000);
             expect(addItemToOrder!.adjustments.length).toBe(0);
 
@@ -478,7 +492,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode,
             });
-
+            orderResultGuard.assertSuccess(applyCouponCode);
             expect(applyCouponCode!.adjustments.length).toBe(1);
             expect(applyCouponCode!.adjustments[0].description).toBe('50% discount on order');
             expect(applyCouponCode!.total).toBe(3000);
@@ -524,6 +538,7 @@ describe('Promotions applied to Orders', () => {
             function getItemSale1Line(lines: TestOrderFragment.Lines[]): TestOrderFragment.Lines {
                 return lines.find(l => l.productVariant.id === getVariantBySlug('item-sale-1').id)!;
             }
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.adjustments.length).toBe(0);
             expect(getItemSale1Line(addItemToOrder!.lines).adjustments.length).toBe(2); // 2x tax
             expect(addItemToOrder!.total).toBe(2640);
@@ -534,6 +549,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode,
             });
+            orderResultGuard.assertSuccess(applyCouponCode);
 
             expect(applyCouponCode!.total).toBe(1920);
             expect(getItemSale1Line(applyCouponCode!.lines).adjustments.length).toBe(4); // 2x tax, 2x promotion
@@ -580,6 +596,7 @@ describe('Promotions applied to Orders', () => {
                 productVariantId: item60.id,
                 quantity: 1,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.adjustments.length).toBe(0);
             expect(addItemToOrder!.lines[0].adjustments.length).toBe(1); // 1x tax
             expect(addItemToOrder!.total).toBe(6000);
@@ -590,6 +607,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode,
             });
+            orderResultGuard.assertSuccess(applyCouponCode);
 
             expect(applyCouponCode!.total).toBe(3000);
             expect(applyCouponCode!.lines[0].adjustments.length).toBe(2); // 1x tax, 1x promotion
@@ -650,6 +668,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode: 'CODE1',
             });
+            orderResultGuard.assertSuccess(apply1);
 
             expect(apply1?.lines[0].adjustments.length).toBe(2);
             expect(
@@ -664,6 +683,7 @@ describe('Promotions applied to Orders', () => {
             >(APPLY_COUPON_CODE, {
                 couponCode: 'CODE2',
             });
+            orderResultGuard.assertSuccess(apply2);
 
             expect(apply2?.lines[0].adjustments.length).toBe(2);
             expect(
@@ -731,6 +751,7 @@ describe('Promotions applied to Orders', () => {
                     ApplyCouponCode.Mutation,
                     ApplyCouponCode.Variables
                 >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertSuccess(applyCouponCode);
 
                 expect(applyCouponCode!.total).toBe(0);
                 expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
@@ -754,22 +775,20 @@ describe('Promotions applied to Orders', () => {
                 ]);
             });
 
-            it('throws when usage exceeds limit', async () => {
+            it('returns error result when usage exceeds limit', async () => {
                 await shopClient.asAnonymousUser();
                 await createNewActiveOrder();
                 await addGuestCustomerToOrder();
 
-                try {
-                    await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
-                        APPLY_COUPON_CODE,
-                        { couponCode: TEST_COUPON_CODE },
-                    );
-                    fail('should have thrown');
-                } catch (err) {
-                    expect(err.message).toEqual(
-                        expect.stringContaining('Coupon code cannot be used more than once per customer'),
-                    );
-                }
+                const { applyCouponCode } = await shopClient.query<
+                    ApplyCouponCode.Mutation,
+                    ApplyCouponCode.Variables
+                >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertErrorResult(applyCouponCode);
+
+                expect(applyCouponCode.message).toEqual(
+                    'Coupon code cannot be used more than once per customer',
+                );
             });
 
             it('removes couponCode from order when adding customer after code applied', async () => {
@@ -780,6 +799,7 @@ describe('Promotions applied to Orders', () => {
                     ApplyCouponCode.Mutation,
                     ApplyCouponCode.Variables
                 >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertSuccess(applyCouponCode);
 
                 expect(applyCouponCode!.total).toBe(0);
                 expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
@@ -804,6 +824,7 @@ describe('Promotions applied to Orders', () => {
                     ApplyCouponCode.Mutation,
                     ApplyCouponCode.Variables
                 >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertSuccess(applyCouponCode);
 
                 expect(applyCouponCode!.total).toBe(0);
                 expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
@@ -814,20 +835,18 @@ describe('Promotions applied to Orders', () => {
                 expect(order.active).toBe(false);
             });
 
-            it('throws when usage exceeds limit', async () => {
+            it('returns error result when usage exceeds limit', async () => {
                 await logInAsRegisteredCustomer();
                 await createNewActiveOrder();
-                try {
-                    await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
-                        APPLY_COUPON_CODE,
-                        { couponCode: TEST_COUPON_CODE },
-                    );
-                    fail('should have thrown');
-                } catch (err) {
-                    expect(err.message).toEqual(
-                        expect.stringContaining('Coupon code cannot be used more than once per customer'),
-                    );
-                }
+                const { applyCouponCode } = await shopClient.query<
+                    ApplyCouponCode.Mutation,
+                    ApplyCouponCode.Variables
+                >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertErrorResult(applyCouponCode);
+                expect(applyCouponCode.message).toEqual(
+                    'Coupon code cannot be used more than once per customer',
+                );
+                expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_LIMIT_ERROR);
             });
 
             it('removes couponCode from order when logging in after code applied', async () => {
@@ -837,6 +856,7 @@ describe('Promotions applied to Orders', () => {
                     ApplyCouponCode.Mutation,
                     ApplyCouponCode.Variables
                 >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
+                orderResultGuard.assertSuccess(applyCouponCode);
 
                 expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
                 expect(applyCouponCode!.total).toBe(0);

+ 13 - 11
packages/core/e2e/price-calculation-strategy.e2e-spec.ts

@@ -86,20 +86,22 @@ const ADD_ITEM_TO_ORDER_CUSTOM_FIELDS = gql`
             quantity: $quantity
             customFields: $customFields
         ) {
-            id
-            subTotalBeforeTax
-            subTotal
-            shipping
-            total
-            lines {
+            ... on Order {
                 id
-                quantity
-                unitPrice
-                unitPriceWithTax
-                items {
+                subTotalBeforeTax
+                subTotal
+                shipping
+                total
+                lines {
+                    id
+                    quantity
                     unitPrice
                     unitPriceWithTax
-                    unitPriceIncludesTax
+                    items {
+                        unitPrice
+                        unitPriceWithTax
+                        unitPriceIncludesTax
+                    }
                 }
             }
         }

+ 311 - 216
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -1,6 +1,6 @@
 /* tslint:disable:no-non-null-assertion */
 import { OnModuleInit } from '@nestjs/common';
-import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
+import { ErrorCode, RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
 import { pick } from '@vendure/common/lib/pick';
 import {
     AccountRegistrationEvent,
@@ -12,7 +12,7 @@ import {
     PasswordResetEvent,
     VendurePlugin,
 } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import path from 'path';
@@ -30,6 +30,7 @@ import {
     Permission,
 } from './graphql/generated-e2e-admin-types';
 import {
+    CurrentUserShopFragment,
     GetActiveCustomer,
     RefreshToken,
     Register,
@@ -70,21 +71,29 @@ let sendEmailFn: jest.Mock;
 class TestEmailPlugin implements OnModuleInit {
     constructor(private eventBus: EventBus) {}
     onModuleInit() {
-        this.eventBus.ofType(AccountRegistrationEvent).subscribe((event) => {
+        this.eventBus.ofType(AccountRegistrationEvent).subscribe(event => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(PasswordResetEvent).subscribe((event) => {
+        this.eventBus.ofType(PasswordResetEvent).subscribe(event => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe((event) => {
+        this.eventBus.ofType(IdentifierChangeRequestEvent).subscribe(event => {
             sendEmailFn(event);
         });
-        this.eventBus.ofType(IdentifierChangeEvent).subscribe((event) => {
+        this.eventBus.ofType(IdentifierChangeEvent).subscribe(event => {
             sendEmailFn(event);
         });
     }
 }
 
+const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard<{
+    success: boolean;
+}>(input => input.success != null);
+
+const currentUserErrorGuard: ErrorResultGuard<CurrentUserShopFragment> = createErrorResultGuard<
+    CurrentUserShopFragment
+>(input => input.identifier != null);
+
 describe('Shop auth & accounts', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
@@ -115,6 +124,24 @@ describe('Shop auth & accounts', () => {
             sendEmailFn = jest.fn();
         });
 
+        it('does not return error result on email address conflict', async () => {
+            // To prevent account enumeration attacks
+            const { customers } = await adminClient.query<GetCustomerList.Query>(GET_CUSTOMER_LIST);
+            const input: RegisterCustomerInput = {
+                firstName: 'Duplicate',
+                lastName: 'Person',
+                phoneNumber: '123456',
+                emailAddress: customers.items[0].emailAddress,
+            };
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
+        });
+
         it('register a new account without password', async () => {
             const verificationTokenPromise = getVerificationTokenPromise();
             const input: RegisterCustomerInput = {
@@ -123,13 +150,17 @@ describe('Shop auth & accounts', () => {
                 phoneNumber: '123456',
                 emailAddress,
             };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
-                input,
-            });
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
 
             verificationToken = await verificationTokenPromise;
 
-            expect(result.registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(verificationToken).toBeDefined();
 
@@ -152,7 +183,7 @@ describe('Shop auth & accounts', () => {
         });
 
         it('issues a new token if attempting to register a second time', async () => {
-            const sendEmail = new Promise<string>((resolve) => {
+            const sendEmail = new Promise<string>(resolve => {
                 sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
                     resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
                 });
@@ -162,13 +193,17 @@ describe('Shop auth & accounts', () => {
                 lastName: 'Tester',
                 emailAddress,
             };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
-                input,
-            });
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
 
             const newVerificationToken = await sendEmail;
 
-            expect(result.registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(newVerificationToken).not.toBe(verificationToken);
 
@@ -176,19 +211,19 @@ describe('Shop auth & accounts', () => {
         });
 
         it('refreshCustomerVerification issues a new token', async () => {
-            const sendEmail = new Promise<string>((resolve) => {
+            const sendEmail = new Promise<string>(resolve => {
                 sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
                     resolve(event.user.getNativeAuthenticationMethod().verificationToken!);
                 });
             });
-            const result = await shopClient.query<RefreshToken.Mutation, RefreshToken.Variables>(
-                REFRESH_TOKEN,
-                { emailAddress },
-            );
-
+            const { refreshCustomerVerification } = await shopClient.query<
+                RefreshToken.Mutation,
+                RefreshToken.Variables
+            >(REFRESH_TOKEN, { emailAddress });
+            successErrorGuard.assertSuccess(refreshCustomerVerification);
             const newVerificationToken = await sendEmail;
 
-            expect(result.refreshCustomerVerification).toBe(true);
+            expect(refreshCustomerVerification.success).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(newVerificationToken).not.toBe(verificationToken);
 
@@ -196,14 +231,16 @@ describe('Shop auth & accounts', () => {
         });
 
         it('refreshCustomerVerification does nothing with an unrecognized emailAddress', async () => {
-            const result = await shopClient.query<RefreshToken.Mutation, RefreshToken.Variables>(
-                REFRESH_TOKEN,
-                {
-                    emailAddress: 'never-been-registered@test.com',
-                },
-            );
+            const { refreshCustomerVerification } = await shopClient.query<
+                RefreshToken.Mutation,
+                RefreshToken.Variables
+            >(REFRESH_TOKEN, {
+                emailAddress: 'never-been-registered@test.com',
+            });
+            successErrorGuard.assertSuccess(refreshCustomerVerification);
             await waitForSendEmailFn();
-            expect(result.refreshCustomerVerification).toBe(true);
+
+            expect(refreshCustomerVerification.success).toBe(true);
             expect(sendEmailFn).not.toHaveBeenCalled();
         });
 
@@ -216,36 +253,44 @@ describe('Shop auth & accounts', () => {
             }
         });
 
-        it(
-            'verification fails with wrong token',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                        password,
-                        token: 'bad-token',
-                    }),
-                `Verification token not recognized`,
-            ),
-        );
+        it('verification fails with wrong token', async () => {
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    password,
+                    token: 'bad-token',
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
 
-        it(
-            'verification fails with no password',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                        token: verificationToken,
-                    }),
-                `A password must be provided as it was not set during registration`,
-            ),
-        );
+            expect(verifyCustomerAccount.message).toBe(`Verification token not recognized`);
+            expect(verifyCustomerAccount.code).toBe(ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR);
+        });
+
+        it('verification fails with no password', async () => {
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    token: verificationToken,
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
+
+            expect(verifyCustomerAccount.message).toBe(`A password must be provided.`);
+            expect(verifyCustomerAccount.code).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
+        });
 
         it('verification succeeds with password and correct token', async () => {
-            const result = await shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                password,
-                token: verificationToken,
-            });
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    password,
+                    token: verificationToken,
+                },
+            );
+            currentUserErrorGuard.assertSuccess(verifyCustomerAccount);
 
-            expect(result.verifyCustomerAccount.user.identifier).toBe('test1@test.com');
+            expect(verifyCustomerAccount.identifier).toBe('test1@test.com');
             const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
             newCustomerId = activeCustomer!.id;
         });
@@ -256,25 +301,32 @@ describe('Shop auth & accounts', () => {
                 lastName: 'Hacker',
                 emailAddress,
             };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
-                input,
-            });
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
+
             await waitForSendEmailFn();
-            expect(result.registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             expect(sendEmailFn).not.toHaveBeenCalled();
         });
 
-        it(
-            'verification fails if attempted a second time',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                        password,
-                        token: verificationToken,
-                    }),
-                `Verification token not recognized`,
-            ),
-        );
+        it('verification fails if attempted a second time', async () => {
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    password,
+                    token: verificationToken,
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
+
+            expect(verifyCustomerAccount.message).toBe(`Verification token not recognized`);
+            expect(verifyCustomerAccount.code).toBe(ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR);
+        });
 
         it('customer history contains entries for registration & verification', async () => {
             const { customer } = await adminClient.query<
@@ -322,13 +374,17 @@ describe('Shop auth & accounts', () => {
                 emailAddress,
                 password,
             };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
-                input,
-            });
+            const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+                REGISTER_ACCOUNT,
+                {
+                    input,
+                },
+            );
+            successErrorGuard.assertSuccess(registerCustomerAccount);
 
             verificationToken = await verificationTokenPromise;
 
-            expect(result.registerCustomerAccount).toBe(true);
+            expect(registerCustomerAccount.success).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(verificationToken).toBeDefined();
 
@@ -349,25 +405,31 @@ describe('Shop auth & accounts', () => {
                 pick(customers.items[0], ['firstName', 'lastName', 'emailAddress', 'phoneNumber']),
             ).toEqual(pick(input, ['firstName', 'lastName', 'emailAddress', 'phoneNumber']));
         });
-        it(
-            'verification fails with password',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                        token: verificationToken,
-                        password: 'new password',
-                    }),
-                `A password has already been set during registration`,
-            ),
-        );
+
+        it('verification fails with password', async () => {
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    token: verificationToken,
+                    password: 'new password',
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
+
+            expect(verifyCustomerAccount.message).toBe(`A password has already been set during registration`);
+            expect(verifyCustomerAccount.code).toBe(ErrorCode.PASSWORD_ALREADY_SET_ERROR);
+        });
 
         it('verification succeeds with no password and correct token', async () => {
-            const a = 1;
-            const result = await shopClient.query<Verify.Mutation, Verify.Variables>(VERIFY_EMAIL, {
-                token: verificationToken,
-            });
+            const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+                VERIFY_EMAIL,
+                {
+                    token: verificationToken,
+                },
+            );
+            currentUserErrorGuard.assertSuccess(verifyCustomerAccount);
 
-            expect(result.verifyCustomerAccount.user.identifier).toBe('test2@test.com');
+            expect(verifyCustomerAccount.identifier).toBe('test2@test.com');
             const { activeCustomer } = await shopClient.query<GetActiveCustomer.Query>(GET_ACTIVE_CUSTOMER);
         });
     });
@@ -388,55 +450,62 @@ describe('Shop auth & accounts', () => {
         });
 
         it('requestPasswordReset silently fails with invalid identifier', async () => {
-            const result = await shopClient.query<
+            const { requestPasswordReset } = await shopClient.query<
                 RequestPasswordReset.Mutation,
                 RequestPasswordReset.Variables
             >(REQUEST_PASSWORD_RESET, {
                 identifier: 'invalid-identifier',
             });
+            successErrorGuard.assertSuccess(requestPasswordReset);
 
             await waitForSendEmailFn();
-            expect(result.requestPasswordReset).toBe(true);
+            expect(requestPasswordReset.success).toBe(true);
             expect(sendEmailFn).not.toHaveBeenCalled();
             expect(passwordResetToken).not.toBeDefined();
         });
 
         it('requestPasswordReset sends reset token', async () => {
             const passwordResetTokenPromise = getPasswordResetTokenPromise();
-            const result = await shopClient.query<
+            const { requestPasswordReset } = await shopClient.query<
                 RequestPasswordReset.Mutation,
                 RequestPasswordReset.Variables
             >(REQUEST_PASSWORD_RESET, {
                 identifier: customer.emailAddress,
             });
+            successErrorGuard.assertSuccess(requestPasswordReset);
 
             passwordResetToken = await passwordResetTokenPromise;
 
-            expect(result.requestPasswordReset).toBe(true);
+            expect(requestPasswordReset.success).toBe(true);
             expect(sendEmailFn).toHaveBeenCalled();
             expect(passwordResetToken).toBeDefined();
         });
 
-        it(
-            'resetPassword fails with wrong token',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(RESET_PASSWORD, {
-                        password: 'newPassword',
-                        token: 'bad-token',
-                    }),
-                `Password reset token not recognized`,
-            ),
-        );
+        it('resetPassword returns error result with wrong token', async () => {
+            const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
+                RESET_PASSWORD,
+                {
+                    password: 'newPassword',
+                    token: 'bad-token',
+                },
+            );
+            currentUserErrorGuard.assertErrorResult(resetPassword);
+
+            expect(resetPassword.message).toBe(`Password reset token not recognized`);
+            expect(resetPassword.code).toBe(ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR);
+        });
 
         it('resetPassword works with valid token', async () => {
-            const result = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
+            const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
                 RESET_PASSWORD,
                 {
                     token: passwordResetToken,
                     password: 'newPassword',
                 },
             );
+            currentUserErrorGuard.assertSuccess(resetPassword);
+
+            expect(resetPassword.identifier).toBe(customer.emailAddress);
 
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
             expect(loginResult.user.identifier).toBe(customer.emailAddress);
@@ -500,41 +569,40 @@ describe('Shop auth & accounts', () => {
             }
         });
 
-        it('throws if password is incorrect', async () => {
-            try {
-                await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
-                await shopClient.query<
-                    RequestUpdateEmailAddress.Mutation,
-                    RequestUpdateEmailAddress.Variables
-                >(REQUEST_UPDATE_EMAIL_ADDRESS, {
-                    password: 'bad password',
-                    newEmailAddress: NEW_EMAIL_ADDRESS,
-                });
-                fail('should have thrown');
-            } catch (err) {
-                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
-            }
+        it('return error result if password is incorrect', async () => {
+            await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
+            const { requestUpdateCustomerEmailAddress } = await shopClient.query<
+                RequestUpdateEmailAddress.Mutation,
+                RequestUpdateEmailAddress.Variables
+            >(REQUEST_UPDATE_EMAIL_ADDRESS, {
+                password: 'bad password',
+                newEmailAddress: NEW_EMAIL_ADDRESS,
+            });
+            successErrorGuard.assertErrorResult(requestUpdateCustomerEmailAddress);
+
+            expect(requestUpdateCustomerEmailAddress.message).toBe('The provided credentials are invalid');
+            expect(requestUpdateCustomerEmailAddress.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
         });
 
-        it(
-            'throws if email address already in use',
-            assertThrowsWithMessage(async () => {
-                await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
-                const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
-                    GET_CUSTOMER,
-                    { id: 'T_2' },
-                );
-                const otherCustomer = result.customer!;
+        it('return error result email address already in use', async () => {
+            await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
+            const result = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
+                id: 'T_2',
+            });
+            const otherCustomer = result.customer!;
+
+            const { requestUpdateCustomerEmailAddress } = await shopClient.query<
+                RequestUpdateEmailAddress.Mutation,
+                RequestUpdateEmailAddress.Variables
+            >(REQUEST_UPDATE_EMAIL_ADDRESS, {
+                password: PASSWORD,
+                newEmailAddress: otherCustomer.emailAddress,
+            });
+            successErrorGuard.assertErrorResult(requestUpdateCustomerEmailAddress);
 
-                await shopClient.query<
-                    RequestUpdateEmailAddress.Mutation,
-                    RequestUpdateEmailAddress.Variables
-                >(REQUEST_UPDATE_EMAIL_ADDRESS, {
-                    password: PASSWORD,
-                    newEmailAddress: otherCustomer.emailAddress,
-                });
-            }, 'This email address is not available'),
-        );
+            expect(requestUpdateCustomerEmailAddress.message).toBe('The email address is not available.');
+            expect(requestUpdateCustomerEmailAddress.code).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
+        });
 
         it('triggers event with token', async () => {
             await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
@@ -564,22 +632,25 @@ describe('Shop auth & accounts', () => {
             }
         });
 
-        it(
-            'throws with bad token',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<UpdateEmailAddress.Mutation, UpdateEmailAddress.Variables>(
-                    UPDATE_EMAIL_ADDRESS,
-                    { token: 'bad token' },
-                );
-            }, 'Identifier change token not recognized'),
-        );
+        it('return error result for bad token', async () => {
+            const { updateCustomerEmailAddress } = await shopClient.query<
+                UpdateEmailAddress.Mutation,
+                UpdateEmailAddress.Variables
+            >(UPDATE_EMAIL_ADDRESS, { token: 'bad token' });
+            successErrorGuard.assertErrorResult(updateCustomerEmailAddress);
+
+            expect(updateCustomerEmailAddress.message).toBe('Identifier change token not recognized');
+            expect(updateCustomerEmailAddress.code).toBe(ErrorCode.IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR);
+        });
 
         it('verify the new email address', async () => {
-            const result = await shopClient.query<UpdateEmailAddress.Mutation, UpdateEmailAddress.Variables>(
-                UPDATE_EMAIL_ADDRESS,
-                { token: emailUpdateToken },
-            );
-            expect(result.updateCustomerEmailAddress).toBe(true);
+            const { updateCustomerEmailAddress } = await shopClient.query<
+                UpdateEmailAddress.Mutation,
+                UpdateEmailAddress.Variables
+            >(UPDATE_EMAIL_ADDRESS, { token: emailUpdateToken });
+            successErrorGuard.assertSuccess(updateCustomerEmailAddress);
+
+            expect(updateCustomerEmailAddress.success).toBe(true);
 
             expect(sendEmailFn).toHaveBeenCalled();
             expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
@@ -699,7 +770,7 @@ describe('Shop auth & accounts', () => {
      * A "sleep" function which allows the sendEmailFn time to get called.
      */
     function waitForSendEmailFn() {
-        return new Promise((resolve) => setTimeout(resolve, 10));
+        return new Promise(resolve => setTimeout(resolve, 10));
     }
 });
 
@@ -730,64 +801,79 @@ describe('Expiring tokens', () => {
         await server.destroy();
     });
 
-    it(
-        'attempting to verify after token has expired throws',
-        assertThrowsWithMessage(async () => {
-            const verificationTokenPromise = getVerificationTokenPromise();
-            const input: RegisterCustomerInput = {
-                firstName: 'Barry',
-                lastName: 'Wallace',
-                emailAddress: 'barry.wallace@test.com',
-            };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
+    it('attempting to verify after token has expired throws', async () => {
+        const verificationTokenPromise = getVerificationTokenPromise();
+        const input: RegisterCustomerInput = {
+            firstName: 'Barry',
+            lastName: 'Wallace',
+            emailAddress: 'barry.wallace@test.com',
+        };
+        const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+            REGISTER_ACCOUNT,
+            {
                 input,
-            });
+            },
+        );
+        successErrorGuard.assertSuccess(registerCustomerAccount);
 
-            const verificationToken = await verificationTokenPromise;
+        const verificationToken = await verificationTokenPromise;
 
-            expect(result.registerCustomerAccount).toBe(true);
-            expect(sendEmailFn).toHaveBeenCalledTimes(1);
-            expect(verificationToken).toBeDefined();
+        expect(registerCustomerAccount.success).toBe(true);
+        expect(sendEmailFn).toHaveBeenCalledTimes(1);
+        expect(verificationToken).toBeDefined();
 
-            await new Promise((resolve) => setTimeout(resolve, 3));
+        await new Promise(resolve => setTimeout(resolve, 3));
 
-            return shopClient.query(VERIFY_EMAIL, {
+        const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
+            VERIFY_EMAIL,
+            {
                 password: 'test',
                 token: verificationToken,
-            });
-        }, `Verification token has expired. Use refreshCustomerVerification to send a new token.`),
-    );
+            },
+        );
+        currentUserErrorGuard.assertErrorResult(verifyCustomerAccount);
 
-    it(
-        'attempting to reset password after token has expired throws',
-        assertThrowsWithMessage(async () => {
-            const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
-                GET_CUSTOMER,
-                { id: 'T_1' },
-            );
+        expect(verifyCustomerAccount.message).toBe(
+            `Verification token has expired. Use refreshCustomerVerification to send a new token.`,
+        );
+        expect(verifyCustomerAccount.code).toBe(ErrorCode.VERIFICATION_TOKEN_EXPIRED_ERROR);
+    });
 
-            const passwordResetTokenPromise = getPasswordResetTokenPromise();
-            const result = await shopClient.query<
-                RequestPasswordReset.Mutation,
-                RequestPasswordReset.Variables
-            >(REQUEST_PASSWORD_RESET, {
-                identifier: customer!.emailAddress,
-            });
+    it('attempting to reset password after token has expired returns error result', async () => {
+        const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(GET_CUSTOMER, {
+            id: 'T_1',
+        });
 
-            const passwordResetToken = await passwordResetTokenPromise;
+        const passwordResetTokenPromise = getPasswordResetTokenPromise();
+        const { requestPasswordReset } = await shopClient.query<
+            RequestPasswordReset.Mutation,
+            RequestPasswordReset.Variables
+        >(REQUEST_PASSWORD_RESET, {
+            identifier: customer!.emailAddress,
+        });
+        successErrorGuard.assertSuccess(requestPasswordReset);
 
-            expect(result.requestPasswordReset).toBe(true);
-            expect(sendEmailFn).toHaveBeenCalledTimes(1);
-            expect(passwordResetToken).toBeDefined();
+        const passwordResetToken = await passwordResetTokenPromise;
 
-            await new Promise((resolve) => setTimeout(resolve, 3));
+        expect(requestPasswordReset.success).toBe(true);
+        expect(sendEmailFn).toHaveBeenCalledTimes(1);
+        expect(passwordResetToken).toBeDefined();
+
+        await new Promise(resolve => setTimeout(resolve, 3));
 
-            return shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(RESET_PASSWORD, {
+        const { resetPassword } = await shopClient.query<ResetPassword.Mutation, ResetPassword.Variables>(
+            RESET_PASSWORD,
+            {
                 password: 'test',
                 token: passwordResetToken,
-            });
-        }, `Password reset token has expired.`),
-    );
+            },
+        );
+
+        currentUserErrorGuard.assertErrorResult(resetPassword);
+
+        expect(resetPassword.message).toBe(`Password reset token has expired`);
+        expect(resetPassword.code).toBe(ErrorCode.PASSWORD_RESET_TOKEN_EXPIRED_ERROR);
+    });
 });
 
 describe('Registration without email verification', () => {
@@ -817,19 +903,23 @@ describe('Registration without email verification', () => {
         await server.destroy();
     });
 
-    it(
-        'errors if no password is provided',
-        assertThrowsWithMessage(async () => {
-            const input: RegisterCustomerInput = {
-                firstName: 'Glen',
-                lastName: 'Beardsley',
-                emailAddress: userEmailAddress,
-            };
-            const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
+    it('Returns error result if no password is provided', async () => {
+        const input: RegisterCustomerInput = {
+            firstName: 'Glen',
+            lastName: 'Beardsley',
+            emailAddress: userEmailAddress,
+        };
+        const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+            REGISTER_ACCOUNT,
+            {
                 input,
-            });
-        }, 'A password must be provided when `authOptions.requireVerification` is set to "false"'),
-    );
+            },
+        );
+        successErrorGuard.assertErrorResult(registerCustomerAccount);
+
+        expect(registerCustomerAccount.message).toBe('A password must be provided.');
+        expect(registerCustomerAccount.code).toBe(ErrorCode.MISSING_PASSWORD_ERROR);
+    });
 
     it('register a new account with password', async () => {
         const input: RegisterCustomerInput = {
@@ -838,11 +928,15 @@ describe('Registration without email verification', () => {
             emailAddress: userEmailAddress,
             password: 'test',
         };
-        const result = await shopClient.query<Register.Mutation, Register.Variables>(REGISTER_ACCOUNT, {
-            input,
-        });
+        const { registerCustomerAccount } = await shopClient.query<Register.Mutation, Register.Variables>(
+            REGISTER_ACCOUNT,
+            {
+                input,
+            },
+        );
+        successErrorGuard.assertSuccess(registerCustomerAccount);
 
-        expect(result.registerCustomerAccount).toBe(true);
+        expect(registerCustomerAccount.success).toBe(true);
         expect(sendEmailFn).not.toHaveBeenCalled();
     });
 
@@ -904,8 +998,9 @@ describe('Updating email address without email verification', () => {
             password: 'test',
             newEmailAddress: NEW_EMAIL_ADDRESS,
         });
+        successErrorGuard.assertSuccess(requestUpdateCustomerEmailAddress);
 
-        expect(requestUpdateCustomerEmailAddress).toBe(true);
+        expect(requestUpdateCustomerEmailAddress.success).toBe(true);
         expect(sendEmailFn).toHaveBeenCalledTimes(1);
         expect(sendEmailFn.mock.calls[0][0] instanceof IdentifierChangeEvent).toBe(true);
 
@@ -915,7 +1010,7 @@ describe('Updating email address without email verification', () => {
 });
 
 function getVerificationTokenPromise(): Promise<string> {
-    return new Promise<any>((resolve) => {
+    return new Promise<any>(resolve => {
         sendEmailFn.mockImplementation((event: AccountRegistrationEvent) => {
             resolve(event.user.getNativeAuthenticationMethod().verificationToken);
         });
@@ -923,7 +1018,7 @@ function getVerificationTokenPromise(): Promise<string> {
 }
 
 function getPasswordResetTokenPromise(): Promise<string> {
-    return new Promise<any>((resolve) => {
+    return new Promise<any>(resolve => {
         sendEmailFn.mockImplementation((event: PasswordResetEvent) => {
             resolve(event.user.getNativeAuthenticationMethod().passwordResetToken);
         });
@@ -934,7 +1029,7 @@ function getEmailUpdateTokenPromise(): Promise<{
     identifierChangeToken: string | null;
     pendingIdentifier: string | null;
 }> {
-    return new Promise((resolve) => {
+    return new Promise(resolve => {
         sendEmailFn.mockImplementation((event: IdentifierChangeRequestEvent) => {
             resolve(
                 pick(event.user.getNativeAuthenticationMethod(), [

+ 31 - 21
packages/core/e2e/shop-customer.e2e-spec.ts

@@ -1,12 +1,12 @@
 /* tslint:disable:no-non-null-assertion */
 import { pick } from '@vendure/common/lib/pick';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 import { skip } from 'rxjs/operators';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
 
 import {
     AttemptLogin,
@@ -19,6 +19,7 @@ import {
     CreateAddressInput,
     CreateAddressShop,
     DeleteAddressShop,
+    ErrorCode,
     UpdateAddressInput,
     UpdateAddressShop,
     UpdateCustomer,
@@ -39,6 +40,10 @@ describe('Shop customers', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
     let customer: GetCustomer.Customer;
 
+    const successErrorGuard: ErrorResultGuard<{ success: boolean }> = createErrorResultGuard<{
+        success: boolean;
+    }>(input => input.success != null);
+
     beforeAll(async () => {
         await server.init({
             initialData,
@@ -252,12 +257,12 @@ describe('Shop customers', () => {
         );
 
         it('deleteCustomerAddress works', async () => {
-            const result = await shopClient.query<DeleteAddressShop.Mutation, DeleteAddressShop.Variables>(
-                DELETE_ADDRESS,
-                { id: 'T_3' },
-            );
+            const { deleteCustomerAddress } = await shopClient.query<
+                DeleteAddressShop.Mutation,
+                DeleteAddressShop.Variables
+            >(DELETE_ADDRESS, { id: 'T_3' });
 
-            expect(result.deleteCustomerAddress).toBe(true);
+            expect(deleteCustomerAddress.success).toBe(true);
         });
 
         it('customer history for CUSTOMER_ADDRESS_DELETED', async () => {
@@ -286,23 +291,28 @@ describe('Shop customers', () => {
             }, 'You are not currently authorized to perform this action'),
         );
 
-        it(
-            'updatePassword fails with incorrect current password',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<UpdatePassword.Mutation, UpdatePassword.Variables>(UPDATE_PASSWORD, {
-                    old: 'wrong',
-                    new: 'test2',
-                });
-            }, 'The credentials did not match. Please check and try again'),
-        );
+        it('updatePassword return error result with incorrect current password', async () => {
+            const { updateCustomerPassword } = await shopClient.query<
+                UpdatePassword.Mutation,
+                UpdatePassword.Variables
+            >(UPDATE_PASSWORD, {
+                old: 'wrong',
+                new: 'test2',
+            });
+            successErrorGuard.assertErrorResult(updateCustomerPassword);
+
+            expect(updateCustomerPassword.message).toBe('The provided credentials are invalid');
+            expect(updateCustomerPassword.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
+        });
 
         it('updatePassword works', async () => {
-            const response = await shopClient.query<UpdatePassword.Mutation, UpdatePassword.Variables>(
-                UPDATE_PASSWORD,
-                { old: 'test', new: 'test2' },
-            );
+            const { updateCustomerPassword } = await shopClient.query<
+                UpdatePassword.Mutation,
+                UpdatePassword.Variables
+            >(UPDATE_PASSWORD, { old: 'test', new: 'test2' });
+            successErrorGuard.assertSuccess(updateCustomerPassword);
 
-            expect(response.updateCustomerPassword).toBe(true);
+            expect(updateCustomerPassword.success).toBe(true);
 
             // Log out and log in with new password
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'test2');

+ 294 - 205
packages/core/e2e/shop-order.e2e-spec.ts

@@ -1,6 +1,7 @@
 /* tslint:disable:no-non-null-assertion */
+import { pick } from '@vendure/common/lib/pick';
 import { mergeConfig } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -21,24 +22,32 @@ import {
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 import {
+    ActiveOrderCustomer,
+    ActiveOrderCustomerFragment,
     AddItemToOrder,
     AddPaymentToOrder,
     AdjustItemQuantity,
+    ErrorCode,
     GetActiveOrder,
     GetActiveOrderPayments,
+    GetActiveOrderWithPayments,
     GetAvailableCountries,
     GetCustomerAddresses,
     GetCustomerOrders,
     GetNextOrderStates,
     GetOrderByCode,
     GetShippingMethods,
+    PaymentDeclinedError,
     RemoveAllOrderLines,
     RemoveItemFromOrder,
     SetBillingAddress,
     SetCustomerForOrder,
     SetShippingAddress,
     SetShippingMethod,
+    TestOrderFragmentFragment,
+    TestOrderWithPaymentsFragment,
     TransitionToState,
+    UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 import {
     ATTEMPT_LOGIN,
@@ -55,6 +64,7 @@ import {
     GET_ACTIVE_ORDER_ADDRESSES,
     GET_ACTIVE_ORDER_ORDERS,
     GET_ACTIVE_ORDER_PAYMENTS,
+    GET_ACTIVE_ORDER_WITH_PAYMENTS,
     GET_AVAILABLE_COUNTRIES,
     GET_ELIGIBLE_SHIPPING_METHODS,
     GET_NEXT_STATES,
@@ -65,6 +75,7 @@ import {
     SET_CUSTOMER,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
+    TEST_ORDER_FRAGMENT,
     TRANSITION_TO_STATE,
 } from './graphql/shop-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
@@ -88,6 +99,15 @@ describe('Shop orders', () => {
         }),
     );
 
+    type OrderSuccessResult =
+        | UpdatedOrderFragment
+        | TestOrderFragmentFragment
+        | TestOrderWithPaymentsFragment
+        | ActiveOrderCustomerFragment;
+    const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard<OrderSuccessResult>(
+        input => !!input.lines,
+    );
+
     beforeAll(async () => {
         await server.init({
             initialData,
@@ -144,6 +164,7 @@ describe('Shop orders', () => {
                 quantity: 1,
             });
 
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].quantity).toBe(1);
             expect(addItemToOrder!.lines[0].productVariant.id).toBe('T_1');
@@ -164,17 +185,19 @@ describe('Shop orders', () => {
             ),
         );
 
-        it(
-            'addItemToOrder errors with a negative quantity',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
-                        productVariantId: 'T_999',
-                        quantity: -3,
-                    }),
-                `-3 is not a valid quantity for an OrderItem`,
-            ),
-        );
+        it('addItemToOrder errors with a negative quantity', async () => {
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_999',
+                quantity: -3,
+            });
+
+            orderResultGuard.assertErrorResult(addItemToOrder);
+            expect(addItemToOrder.message).toEqual(`The quantity for an OrderItem cannot be negative`);
+            expect(addItemToOrder.errorCode).toEqual(ErrorCode.NEGATIVE_QUANTITY_ERROR);
+        });
 
         it('addItemToOrder with an existing productVariantId adds quantity to the existing OrderLine', async () => {
             const { addItemToOrder } = await shopClient.query<
@@ -184,20 +207,26 @@ describe('Shop orders', () => {
                 productVariantId: 'T_1',
                 quantity: 2,
             });
-
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].quantity).toBe(3);
         });
 
-        it(
-            'addItemToOrder errors when going beyond orderItemsLimit',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
-                    productVariantId: 'T_1',
-                    quantity: 100,
-                });
-            }, 'Cannot add items. An order may consist of a maximum of 99 items'),
-        );
+        it('addItemToOrder errors when going beyond orderItemsLimit', async () => {
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 100,
+            });
+
+            orderResultGuard.assertErrorResult(addItemToOrder);
+            expect(addItemToOrder.message).toBe(
+                'Cannot add items. An order may consist of a maximum of 99 items',
+            );
+            expect(addItemToOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
+        });
 
         it('adjustOrderLine adjusts the quantity', async () => {
             const { adjustOrderLine } = await shopClient.query<
@@ -207,7 +236,7 @@ describe('Shop orders', () => {
                 orderLineId: firstOrderLineId,
                 quantity: 50,
             });
-
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.lines.length).toBe(1);
             expect(adjustOrderLine!.lines[0].quantity).toBe(50);
         });
@@ -220,6 +249,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_3',
                 quantity: 3,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(2);
             expect(addItemToOrder!.lines.map(i => i.productVariant.id)).toEqual(['T_1', 'T_3']);
 
@@ -230,38 +260,38 @@ describe('Shop orders', () => {
                 orderLineId: addItemToOrder?.lines[1].id!,
                 quantity: 0,
             });
-
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.lines.length).toBe(1);
             expect(adjustOrderLine!.lines.map(i => i.productVariant.id)).toEqual(['T_1']);
         });
 
-        it(
-            'adjustOrderLine errors when going beyond orderItemsLimit',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>(
-                    ADJUST_ITEM_QUANTITY,
-                    {
-                        orderLineId: firstOrderLineId,
-                        quantity: 100,
-                    },
-                );
-            }, 'Cannot add items. An order may consist of a maximum of 99 items'),
-        );
+        it('adjustOrderLine errors when going beyond orderItemsLimit', async () => {
+            const { adjustOrderLine } = await shopClient.query<
+                AdjustItemQuantity.Mutation,
+                AdjustItemQuantity.Variables
+            >(ADJUST_ITEM_QUANTITY, {
+                orderLineId: firstOrderLineId,
+                quantity: 100,
+            });
+            orderResultGuard.assertErrorResult(adjustOrderLine);
+            expect(adjustOrderLine.message).toBe(
+                'Cannot add items. An order may consist of a maximum of 99 items',
+            );
+            expect(adjustOrderLine.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
+        });
 
-        it(
-            'adjustOrderLine errors with a negative quantity',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>(
-                        ADJUST_ITEM_QUANTITY,
-                        {
-                            orderLineId: firstOrderLineId,
-                            quantity: -3,
-                        },
-                    ),
-                `-3 is not a valid quantity for an OrderItem`,
-            ),
-        );
+        it('adjustOrderLine errors with a negative quantity', async () => {
+            const { adjustOrderLine } = await shopClient.query<
+                AdjustItemQuantity.Mutation,
+                AdjustItemQuantity.Variables
+            >(ADJUST_ITEM_QUANTITY, {
+                orderLineId: firstOrderLineId,
+                quantity: -3,
+            });
+            orderResultGuard.assertErrorResult(adjustOrderLine);
+            expect(adjustOrderLine.message).toBe('The quantity for an OrderItem cannot be negative');
+            expect(adjustOrderLine.errorCode).toBe(ErrorCode.NEGATIVE_QUANTITY_ERROR);
+        });
 
         it(
             'adjustOrderLine errors with an invalid orderLineId',
@@ -286,6 +316,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_3',
                 quantity: 3,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(2);
             expect(addItemToOrder!.lines.map(i => i.productVariant.id)).toEqual(['T_1', 'T_3']);
 
@@ -295,6 +326,7 @@ describe('Shop orders', () => {
             >(REMOVE_ITEM_FROM_ORDER, {
                 orderLineId: firstOrderLineId,
             });
+            orderResultGuard.assertSuccess(removeOrderLine);
             expect(removeOrderLine!.lines.length).toBe(1);
             expect(removeOrderLine!.lines.map(i => i.productVariant.id)).toEqual(['T_3']);
         });
@@ -319,29 +351,50 @@ describe('Shop orders', () => {
             expect(result.nextOrderStates).toEqual(['ArrangingPayment', 'Cancelled']);
         });
 
-        it(
-            'transitionOrderToState throws for an invalid state',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
-                        TRANSITION_TO_STATE,
-                        { state: 'Completed' },
-                    ),
+        it('transitionOrderToState returns error result for invalid state', async () => {
+            const { transitionOrderToState } = await shopClient.query<
+                TransitionToState.Mutation,
+                TransitionToState.Variables
+            >(TRANSITION_TO_STATE, { state: 'Completed' });
+            orderResultGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.message).toBe(
                 `Cannot transition Order from "AddingItems" to "Completed"`,
-            ),
-        );
+            );
+            expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
+        });
 
-        it(
-            'attempting to transition to ArrangingPayment throws when Order has no Customer',
-            assertThrowsWithMessage(
-                () =>
-                    shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
-                        TRANSITION_TO_STATE,
-                        { state: 'ArrangingPayment' },
-                    ),
+        it('attempting to transition to ArrangingPayment returns error result when Order has no Customer', async () => {
+            const { transitionOrderToState } = await shopClient.query<
+                TransitionToState.Mutation,
+                TransitionToState.Variables
+            >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+            orderResultGuard.assertErrorResult(transitionOrderToState);
+
+            expect(transitionOrderToState!.transitionError).toBe(
                 `Cannot transition Order to the "ArrangingPayment" state without Customer details`,
-            ),
-        );
+            );
+            expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
+        });
+
+        it('setCustomerForOrder returns error result on email address conflict', async () => {
+            const { customers } = await adminClient.query<GetCustomerList.Query>(GET_CUSTOMER_LIST);
+
+            const { setCustomerForOrder } = await shopClient.query<
+                SetCustomerForOrder.Mutation,
+                SetCustomerForOrder.Variables
+            >(SET_CUSTOMER, {
+                input: {
+                    emailAddress: customers.items[0].emailAddress,
+                    firstName: 'Test',
+                    lastName: 'Person',
+                },
+            });
+            orderResultGuard.assertErrorResult(setCustomerForOrder);
+
+            expect(setCustomerForOrder!.message).toBe('The email address is not available.');
+            expect(setCustomerForOrder!.errorCode).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
+        });
 
         it('setCustomerForOrder creates a new Customer and associates it with the Order', async () => {
             const { setCustomerForOrder } = await shopClient.query<
@@ -354,6 +407,7 @@ describe('Shop orders', () => {
                     lastName: 'Person',
                 },
             });
+            orderResultGuard.assertSuccess(setCustomerForOrder);
 
             const customer = setCustomerForOrder!.customer!;
             expect(customer.firstName).toBe('Test');
@@ -373,6 +427,7 @@ describe('Shop orders', () => {
                     lastName: 'Person',
                 },
             });
+            orderResultGuard.assertSuccess(setCustomerForOrder);
 
             const customer = setCustomerForOrder!.customer!;
             expect(customer.firstName).toBe('Changed');
@@ -456,12 +511,16 @@ describe('Shop orders', () => {
         });
 
         it('can transition to ArrangingPayment once Customer has been set', async () => {
-            const result = await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
-                TRANSITION_TO_STATE,
-                { state: 'ArrangingPayment' },
-            );
-
-            expect(result.transitionOrderToState).toEqual({ id: 'T_1', state: 'ArrangingPayment' });
+            const { transitionOrderToState } = await shopClient.query<
+                TransitionToState.Mutation,
+                TransitionToState.Variables
+            >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
+            orderResultGuard.assertSuccess(transitionOrderToState);
+
+            expect(pick(transitionOrderToState, ['id', 'state'])).toEqual({
+                id: 'T_1',
+                state: 'ArrangingPayment',
+            });
         });
 
         it('adds a successful payment and transitions Order state', async () => {
@@ -474,6 +533,7 @@ describe('Shop orders', () => {
                     metadata: {},
                 },
             });
+            orderResultGuard.assertSuccess(addPaymentToOrder);
 
             const payment = addPaymentToOrder!.payments![0];
             expect(addPaymentToOrder!.state).toBe('PaymentSettled');
@@ -505,7 +565,7 @@ describe('Shop orders', () => {
 
     describe('ordering as authenticated user', () => {
         let firstOrderLineId: string;
-        let activeOrder: AddItemToOrder.AddItemToOrder;
+        let activeOrder: UpdatedOrderFragment;
         let authenticatedUserEmailAddress: string;
         let customers: GetCustomerList.Items[];
         const password = 'test';
@@ -538,7 +598,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_1',
                 quantity: 1,
             });
-
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].quantity).toBe(1);
             expect(addItemToOrder!.lines[0].productVariant.id).toBe('T_1');
@@ -568,7 +628,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_1',
                 quantity: 2,
             });
-
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].quantity).toBe(3);
         });
@@ -581,7 +641,7 @@ describe('Shop orders', () => {
                 orderLineId: firstOrderLineId,
                 quantity: 50,
             });
-
+            orderResultGuard.assertSuccess(adjustOrderLine);
             expect(adjustOrderLine!.lines.length).toBe(1);
             expect(adjustOrderLine!.lines[0].quantity).toBe(50);
         });
@@ -594,6 +654,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_3',
                 quantity: 3,
             });
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(2);
             expect(addItemToOrder!.lines.map(i => i.productVariant.id)).toEqual(['T_1', 'T_3']);
 
@@ -603,6 +664,7 @@ describe('Shop orders', () => {
             >(REMOVE_ITEM_FROM_ORDER, {
                 orderLineId: firstOrderLineId,
             });
+            orderResultGuard.assertSuccess(removeOrderLine);
             expect(removeOrderLine!.lines.length).toBe(1);
             expect(removeOrderLine!.lines.map(i => i.productVariant.id)).toEqual(['T_3']);
         });
@@ -623,21 +685,24 @@ describe('Shop orders', () => {
             expect(result2.activeOrder!.id).toBe(activeOrder.id);
         });
 
-        it(
-            'cannot setCustomerForOrder when already logged in',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
-                    SET_CUSTOMER,
-                    {
-                        input: {
-                            emailAddress: 'newperson@email.com',
-                            firstName: 'New',
-                            lastName: 'Person',
-                        },
-                    },
-                );
-            }, 'Cannot set a Customer for the Order when already logged in'),
-        );
+        it('cannot setCustomerForOrder when already logged in', async () => {
+            const { setCustomerForOrder } = await shopClient.query<
+                SetCustomerForOrder.Mutation,
+                SetCustomerForOrder.Variables
+            >(SET_CUSTOMER, {
+                input: {
+                    emailAddress: 'newperson@email.com',
+                    firstName: 'New',
+                    lastName: 'Person',
+                },
+            });
+            orderResultGuard.assertErrorResult(setCustomerForOrder);
+
+            expect(setCustomerForOrder!.message).toBe(
+                'Cannot set a Customer for the Order when already logged in',
+            );
+            expect(setCustomerForOrder!.errorCode).toBe(ErrorCode.ALREADY_LOGGED_IN_ERROR);
+        });
 
         describe('shipping', () => {
             let shippingMethods: GetShippingMethods.EligibleShippingMethods[];
@@ -738,7 +803,7 @@ describe('Shop orders', () => {
                     orderLineId: activeOrder.lines[0].id,
                     quantity: 10,
                 });
-
+                orderResultGuard.assertSuccess(adjustOrderLine);
                 expect(adjustOrderLine!.shipping).toBe(shippingMethods[1].price);
                 expect(adjustOrderLine!.shippingMethod!.id).toBe(shippingMethods[1].id);
                 expect(adjustOrderLine!.shippingMethod!.description).toBe(shippingMethods[1].description);
@@ -746,93 +811,110 @@ describe('Shop orders', () => {
         });
 
         describe('payment', () => {
-            it(
-                'attempting add a Payment throws error when in AddingItems state',
-                assertThrowsWithMessage(
-                    () =>
-                        shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
-                            ADD_PAYMENT,
-                            {
-                                input: {
-                                    method: testSuccessfulPaymentMethod.code,
-                                    metadata: {},
-                                },
-                            },
-                        ),
+            it('attempting add a Payment returns error result when in AddingItems state', async () => {
+                const { addPaymentToOrder } = await shopClient.query<
+                    AddPaymentToOrder.Mutation,
+                    AddPaymentToOrder.Variables
+                >(ADD_PAYMENT, {
+                    input: {
+                        method: testSuccessfulPaymentMethod.code,
+                        metadata: {},
+                    },
+                });
+
+                orderResultGuard.assertErrorResult(addPaymentToOrder);
+                expect(addPaymentToOrder!.message).toBe(
                     `A Payment may only be added when Order is in "ArrangingPayment" state`,
-                ),
-            );
+                );
+                expect(addPaymentToOrder!.errorCode).toBe(ErrorCode.ORDER_PAYMENT_STATE_ERROR);
+            });
 
             it('transitions to the ArrangingPayment state', async () => {
-                const result = await shopClient.query<
+                const { transitionOrderToState } = await shopClient.query<
                     TransitionToState.Mutation,
                     TransitionToState.Variables
                 >(TRANSITION_TO_STATE, { state: 'ArrangingPayment' });
-                expect(result.transitionOrderToState).toEqual({
+
+                orderResultGuard.assertSuccess(transitionOrderToState);
+                expect(pick(transitionOrderToState, ['id', 'state'])).toEqual({
                     id: activeOrder.id,
                     state: 'ArrangingPayment',
                 });
             });
 
-            it(
-                'attempting to add an item throws error when in ArrangingPayment state',
-                assertThrowsWithMessage(
-                    () =>
-                        shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
-                            ADD_ITEM_TO_ORDER,
-                            {
-                                productVariantId: 'T_4',
-                                quantity: 1,
-                            },
-                        ),
+            it('attempting to add an item returns error result when in ArrangingPayment state', async () => {
+                const { addItemToOrder } = await shopClient.query<
+                    AddItemToOrder.Mutation,
+                    AddItemToOrder.Variables
+                >(ADD_ITEM_TO_ORDER, {
+                    productVariantId: 'T_4',
+                    quantity: 1,
+                });
+
+                orderResultGuard.assertErrorResult(addItemToOrder);
+                expect(addItemToOrder.message).toBe(
                     `Order contents may only be modified when in the "AddingItems" state`,
-                ),
-            );
+                );
+                expect(addItemToOrder.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_ERROR);
+            });
 
-            it(
-                'attempting to modify item quantity throws error when in ArrangingPayment state',
-                assertThrowsWithMessage(
-                    () =>
-                        shopClient.query<AdjustItemQuantity.Mutation, AdjustItemQuantity.Variables>(
-                            ADJUST_ITEM_QUANTITY,
-                            {
-                                orderLineId: activeOrder.lines[0].id,
-                                quantity: 12,
-                            },
-                        ),
+            it('attempting to modify item quantity returns error result when in ArrangingPayment state', async () => {
+                const { adjustOrderLine } = await shopClient.query<
+                    AdjustItemQuantity.Mutation,
+                    AdjustItemQuantity.Variables
+                >(ADJUST_ITEM_QUANTITY, {
+                    orderLineId: activeOrder.lines[0].id,
+                    quantity: 12,
+                });
+                orderResultGuard.assertErrorResult(adjustOrderLine);
+                expect(adjustOrderLine.message).toBe(
                     `Order contents may only be modified when in the "AddingItems" state`,
-                ),
-            );
+                );
+                expect(adjustOrderLine.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_ERROR);
+            });
 
-            it(
-                'attempting to remove an item throws error when in ArrangingPayment state',
-                assertThrowsWithMessage(
-                    () =>
-                        shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
-                            REMOVE_ITEM_FROM_ORDER,
-                            {
-                                orderLineId: activeOrder.lines[0].id,
-                            },
-                        ),
+            it('attempting to remove an item returns error result when in ArrangingPayment state', async () => {
+                const { removeOrderLine } = await shopClient.query<
+                    RemoveItemFromOrder.Mutation,
+                    RemoveItemFromOrder.Variables
+                >(REMOVE_ITEM_FROM_ORDER, {
+                    orderLineId: activeOrder.lines[0].id,
+                });
+                orderResultGuard.assertErrorResult(removeOrderLine);
+                expect(removeOrderLine.message).toBe(
                     `Order contents may only be modified when in the "AddingItems" state`,
-                ),
-            );
+                );
+                expect(removeOrderLine.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_ERROR);
+            });
 
-            it(
-                'attempting to setOrderShippingMethod throws error when in ArrangingPayment state',
-                assertThrowsWithMessage(async () => {
-                    const shippingMethodsResult = await shopClient.query<GetShippingMethods.Query>(
-                        GET_ELIGIBLE_SHIPPING_METHODS,
-                    );
-                    const shippingMethods = shippingMethodsResult.eligibleShippingMethods;
-                    return shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
-                        SET_SHIPPING_METHOD,
-                        {
-                            id: shippingMethods[0].id,
-                        },
-                    );
-                }, `Order contents may only be modified when in the "AddingItems" state`),
-            );
+            it('attempting to remove all items returns error result when in ArrangingPayment state', async () => {
+                const { removeAllOrderLines } = await shopClient.query<RemoveAllOrderLines.Mutation>(
+                    REMOVE_ALL_ORDER_LINES,
+                );
+                orderResultGuard.assertErrorResult(removeAllOrderLines);
+                expect(removeAllOrderLines.message).toBe(
+                    `Order contents may only be modified when in the "AddingItems" state`,
+                );
+                expect(removeAllOrderLines.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_ERROR);
+            });
+
+            it('attempting to setOrderShippingMethod returns error result when in ArrangingPayment state', async () => {
+                const shippingMethodsResult = await shopClient.query<GetShippingMethods.Query>(
+                    GET_ELIGIBLE_SHIPPING_METHODS,
+                );
+                const shippingMethods = shippingMethodsResult.eligibleShippingMethods;
+                const { setOrderShippingMethod } = await shopClient.query<
+                    SetShippingMethod.Mutation,
+                    SetShippingMethod.Variables
+                >(SET_SHIPPING_METHOD, {
+                    id: shippingMethods[0].id,
+                });
+                orderResultGuard.assertErrorResult(setOrderShippingMethod);
+                expect(setOrderShippingMethod.message).toBe(
+                    `Order contents may only be modified when in the "AddingItems" state`,
+                );
+                expect(setOrderShippingMethod.errorCode).toBe(ErrorCode.ORDER_MODIFICATION_ERROR);
+            });
 
             it('adds a declined payment', async () => {
                 const { addPaymentToOrder } = await shopClient.query<
@@ -846,9 +928,17 @@ describe('Shop orders', () => {
                         },
                     },
                 });
+                orderResultGuard.assertErrorResult(addPaymentToOrder);
 
-                const payment = addPaymentToOrder!.payments![0];
-                expect(addPaymentToOrder!.payments!.length).toBe(1);
+                expect(addPaymentToOrder!.message).toBe('The payment was declined');
+                expect(addPaymentToOrder!.errorCode).toBe(ErrorCode.PAYMENT_DECLINED_ERROR);
+                expect((addPaymentToOrder as any).paymentErrorMessage).toBe('Insufficient funds');
+
+                const { activeOrder: order } = await shopClient.query<GetActiveOrderWithPayments.Query>(
+                    GET_ACTIVE_ORDER_WITH_PAYMENTS,
+                );
+                const payment = order!.payments![0];
+                expect(order!.payments!.length).toBe(1);
                 expect(payment.method).toBe(testFailingPaymentMethod.code);
                 expect(payment.state).toBe('Declined');
                 expect(payment.transactionId).toBe(null);
@@ -857,25 +947,24 @@ describe('Shop orders', () => {
                 });
             });
 
-            it('adds an error payment and returns error response', async () => {
-                try {
-                    await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(
-                        ADD_PAYMENT,
-                        {
-                            input: {
-                                method: testErrorPaymentMethod.code,
-                                metadata: {
-                                    foo: 'bar',
-                                },
-                            },
+            it('adds an error payment and returns error result', async () => {
+                const { addPaymentToOrder } = await shopClient.query<
+                    AddPaymentToOrder.Mutation,
+                    AddPaymentToOrder.Variables
+                >(ADD_PAYMENT, {
+                    input: {
+                        method: testErrorPaymentMethod.code,
+                        metadata: {
+                            foo: 'bar',
                         },
-                    );
-                    // TODO: we need to return an Error response as per
-                    // https://github.com/vendure-ecommerce/vendure/issues/437
-                    // fail('should have thrown');
-                } catch (err) {
-                    // expect(err.message).toEqual('Something went horribly wrong');
-                }
+                    },
+                });
+
+                orderResultGuard.assertErrorResult(addPaymentToOrder);
+                expect(addPaymentToOrder!.message).toBe('The payment failed');
+                expect(addPaymentToOrder!.errorCode).toBe(ErrorCode.PAYMENT_FAILED_ERROR);
+                expect((addPaymentToOrder as any).paymentErrorMessage).toBe('Something went horribly wrong');
+
                 const result = await shopClient.query<GetActiveOrderPayments.Query>(
                     GET_ACTIVE_ORDER_PAYMENTS,
                 );
@@ -898,6 +987,7 @@ describe('Shop orders', () => {
                         },
                     },
                 });
+                orderResultGuard.assertSuccess(addPaymentToOrder);
 
                 const payment = addPaymentToOrder!.payments![2];
                 expect(addPaymentToOrder!.state).toBe('PaymentSettled');
@@ -979,7 +1069,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_1',
                 quantity: 1,
             });
-
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].productVariant.id).toBe('T_1');
 
@@ -1002,7 +1092,7 @@ describe('Shop orders', () => {
                 productVariantId: 'T_2',
                 quantity: 1,
             });
-
+            orderResultGuard.assertSuccess(addItemToOrder);
             expect(addItemToOrder!.lines.length).toBe(1);
             expect(addItemToOrder!.lines[0].productVariant.id).toBe('T_2');
 
@@ -1069,23 +1159,21 @@ describe('Shop orders', () => {
                 quantity: 1,
             });
 
-            try {
-                await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
-                    SET_CUSTOMER,
-                    {
-                        input: {
-                            emailAddress: customers[0].emailAddress,
-                            firstName: 'Evil',
-                            lastName: 'Hacker',
-                        },
-                    },
-                );
-                fail('Should have thrown');
-            } catch (e) {
-                expect(e.message).toContain(
-                    'Cannot use a registered email address for a guest order. Please log in first',
-                );
-            }
+            const { setCustomerForOrder } = await shopClient.query<
+                SetCustomerForOrder.Mutation,
+                SetCustomerForOrder.Variables
+            >(SET_CUSTOMER, {
+                input: {
+                    emailAddress: customers[0].emailAddress,
+                    firstName: 'Evil',
+                    lastName: 'Hacker',
+                },
+            });
+            orderResultGuard.assertErrorResult(setCustomerForOrder);
+
+            expect(setCustomerForOrder!.message).toBe('The email address is not available.');
+            expect(setCustomerForOrder!.errorCode).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
+
             const { customer } = await adminClient.query<GetCustomer.Query, GetCustomer.Variables>(
                 GET_CUSTOMER,
                 {
@@ -1201,6 +1289,7 @@ describe('Shop orders', () => {
                 RemoveAllOrderLines.Mutation,
                 RemoveAllOrderLines.Variables
             >(REMOVE_ALL_ORDER_LINES);
+            orderResultGuard.assertSuccess(removeAllOrderLines);
             expect(removeAllOrderLines?.total).toBe(0);
             expect(removeAllOrderLines?.lines.length).toBe(0);
         });

+ 7 - 0
packages/core/src/api/api.module.ts

@@ -3,6 +3,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
 import path from 'path';
 
 import { DataImportModule } from '../data-import/data-import.module';
+import { I18nModule } from '../i18n/i18n.module';
 import { ServiceModule } from '../service/service.module';
 
 import { AdminApiModule, ApiSharedModule, ShopApiModule } from './api-internal-modules';
@@ -11,6 +12,7 @@ import { configureGraphQLModule } from './config/configure-graphql-module';
 import { AuthGuard } from './middleware/auth-guard';
 import { ExceptionLoggerFilter } from './middleware/exception-logger.filter';
 import { IdInterceptor } from './middleware/id-interceptor';
+import { TranslateErrorResultInterceptor } from './middleware/translate-error-result-interceptor';
 import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fields-interceptor';
 
 /**
@@ -22,6 +24,7 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
     imports: [
         ServiceModule.forRoot(),
         DataImportModule,
+        I18nModule,
         ApiSharedModule,
         AdminApiModule,
         ShopApiModule,
@@ -60,6 +63,10 @@ import { ValidateCustomFieldsInterceptor } from './middleware/validate-custom-fi
             provide: APP_INTERCEPTOR,
             useClass: ValidateCustomFieldsInterceptor,
         },
+        {
+            provide: APP_INTERCEPTOR,
+            useClass: TranslateErrorResultInterceptor,
+        },
         {
             provide: APP_FILTER,
             useClass: ExceptionLoggerFilter,

+ 5 - 1
packages/core/src/api/config/generate-error-code-enum.ts

@@ -23,7 +23,11 @@ export function generateErrorCodeEnum(typeDefsOrSchema: string | GraphQLSchema):
 
     const errorCodeEnum = `
         extend enum ErrorCode {
-            ${errorNodes.map(n => n?.name.value).join('\n')}
+            ${errorNodes.map(n => camelToUpperSnakeCase(n?.name.value || '')).join('\n')}
         }`;
     return extendSchema(schema, parse(errorCodeEnum));
 }
+
+function camelToUpperSnakeCase(input: string): string {
+    return input.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
+}

+ 40 - 0
packages/core/src/api/middleware/translate-error-result-interceptor.ts

@@ -0,0 +1,40 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { Observable } from 'rxjs';
+import { map, switchMap } from 'rxjs/operators';
+
+import { ErrorResult as AdminErrorResult } from '../../common/error/generated-graphql-admin-errors';
+import { ErrorResult as ShopErrorResult } from '../../common/error/generated-graphql-shop-errors';
+import { I18nService } from '../../i18n/i18n.service';
+import { parseContext } from '../common/parse-context';
+
+/**
+ * @description
+ * Translates any top-level ErrorResult message
+ */
+@Injectable()
+export class TranslateErrorResultInterceptor implements NestInterceptor {
+    constructor(private i18nService: I18nService) {}
+
+    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
+        const { isGraphQL, req } = parseContext(context);
+        return next.handle().pipe(
+            switchMap(result => Promise.resolve(result)),
+            map(result => {
+                if (Array.isArray(result)) {
+                    for (const item of result) {
+                        this.translateResult(req, item);
+                    }
+                } else {
+                    this.translateResult(req, result);
+                }
+                return result;
+            }),
+        );
+    }
+
+    private translateResult(req: any, result: unknown) {
+        if (result instanceof AdminErrorResult || result instanceof ShopErrorResult) {
+            this.i18nService.translateErrorResult(req, result);
+        }
+    }
+}

+ 0 - 5
packages/core/src/api/resolvers/admin/asset.resolver.ts

@@ -41,11 +41,6 @@ export class AssetResolver {
         @Ctx() ctx: RequestContext,
         @Args() args: MutationCreateAssetsArgs,
     ): Promise<CreateAssetResult[]> {
-        // TODO: Currently we validate _all_ mime types up-front due to limitations
-        // with the existing error handling mechanisms. With a solution as described
-        // in https://github.com/vendure-ecommerce/vendure/issues/437 we could defer
-        // this check to the individual processing of a single Asset.
-        // await this.assetService.validateInputMimeTypes(args.input);
         // TODO: Is there some way to parellelize this while still preserving
         // the order of files in the upload? Non-deterministic IDs mess up the e2e test snapshots.
         const assets: CreateAssetResult[] = [];

+ 4 - 2
packages/core/src/api/resolvers/admin/customer.resolver.ts

@@ -14,6 +14,7 @@ import {
     Permission,
     QueryCustomerArgs,
     QueryCustomersArgs,
+    Success,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
@@ -98,9 +99,10 @@ export class CustomerResolver {
     async deleteCustomerAddress(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationDeleteCustomerAddressArgs,
-    ): Promise<boolean> {
+    ): Promise<Success> {
         const { id } = args;
-        return this.customerService.deleteAddress(ctx, id);
+        const success = await this.customerService.deleteAddress(ctx, id);
+        return { success };
     }
 
     @Transaction()

+ 4 - 3
packages/core/src/api/resolvers/base/base-auth.resolver.ts

@@ -7,7 +7,8 @@ import {
 } from '@vendure/common/lib/generated-types';
 import { Request, Response } from 'express';
 
-import { ForbiddenError, InternalServerError, UnauthorizedError } from '../../../common/error/errors';
+import { ForbiddenError, UnauthorizedError } from '../../../common/error/errors';
+import { InvalidCredentialsError } from '../../../common/error/generated-graphql-shop-errors';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../../config/config.service';
 import { User } from '../../../entity/user/user.entity';
@@ -119,10 +120,10 @@ export class BaseAuthResolver {
         ctx: RequestContext,
         currentPassword: string,
         newPassword: string,
-    ): Promise<boolean> {
+    ): Promise<boolean | InvalidCredentialsError> {
         const { activeUserId } = ctx;
         if (!activeUserId) {
-            throw new InternalServerError(`error.no-active-user-id`);
+            throw new ForbiddenError();
         }
         return this.userService.updatePassword(ctx, activeUserId, currentPassword, newPassword);
     }

+ 130 - 64
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -1,5 +1,6 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    ErrorCode,
     LoginResult,
     MutationAuthenticateArgs,
     MutationLoginArgs,
@@ -12,16 +13,21 @@ import {
     MutationUpdateCustomerPasswordArgs,
     MutationVerifyCustomerAccountArgs,
     Permission,
+    RefreshCustomerVerificationResult,
+    RegisterCustomerAccountResult,
+    RequestPasswordResetResult,
+    RequestUpdateCustomerEmailAddressResult,
+    ResetPasswordResult,
+    UpdateCustomerEmailAddressResult,
+    UpdateCustomerPasswordResult,
+    VerifyCustomerAccountResult,
 } from '@vendure/common/lib/generated-shop-types';
 import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 import { Request, Response } from 'express';
 
-import {
-    ForbiddenError,
-    InternalServerError,
-    PasswordResetTokenError,
-    VerificationTokenError,
-} from '../../../common/error/errors';
+import { isGraphQlErrorResult } from '../../../common/error/error-result';
+import { ForbiddenError } from '../../../common/error/errors';
+import { NativeAuthStrategyError } from '../../../common/error/generated-graphql-shop-errors';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../../config/config.service';
 import { Logger } from '../../../config/logger/vendure-logger';
@@ -103,9 +109,21 @@ export class ShopAuthResolver extends BaseAuthResolver {
     async registerCustomerAccount(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationRegisterCustomerAccountArgs,
-    ) {
-        this.requireNativeAuthStrategy();
-        return this.customerService.registerCustomerAccount(ctx, args.input).then(() => true);
+    ): Promise<RegisterCustomerAccountResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        const result = await this.customerService.registerCustomerAccount(ctx, args.input);
+        if (isGraphQlErrorResult(result)) {
+            if (result.code === ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR) {
+                // We do not want to reveal the email address conflict,
+                // otherwise account enumeration attacks become possible.
+                return { success: true };
+            }
+            return result;
+        }
+        return { success: true };
     }
 
     @Transaction()
@@ -116,33 +134,36 @@ export class ShopAuthResolver extends BaseAuthResolver {
         @Args() args: MutationVerifyCustomerAccountArgs,
         @Context('req') req: Request,
         @Context('res') res: Response,
-    ): Promise<LoginResult> {
-        this.requireNativeAuthStrategy();
+    ): Promise<VerifyCustomerAccountResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
         const { token, password } = args;
         const customer = await this.customerService.verifyCustomerEmailAddress(
             ctx,
             token,
             password || undefined,
         );
-        if (customer && customer.user) {
-            const session = await this.authService.createAuthenticatedSessionForUser(
-                ctx,
-                customer.user,
-                NATIVE_AUTH_STRATEGY_NAME,
-            );
-            setSessionToken({
-                req,
-                res,
-                authOptions: this.configService.authOptions,
-                rememberMe: true,
-                sessionToken: session.token,
-            });
-            return {
-                user: this.publiclyAccessibleUser(session.user),
-            };
-        } else {
-            throw new VerificationTokenError();
+        if (isGraphQlErrorResult(customer)) {
+            return customer;
         }
+        const session = await this.authService.createAuthenticatedSessionForUser(
+            ctx,
+            // We know that there is a user, since the Customer
+            // was found with the .getCustomerByUserId() method.
+            // tslint:disable-next-line:no-non-null-assertion
+            customer.user!,
+            NATIVE_AUTH_STRATEGY_NAME,
+        );
+        setSessionToken({
+            req,
+            res,
+            authOptions: this.configService.authOptions,
+            rememberMe: true,
+            sessionToken: session.token,
+        });
+        return this.publiclyAccessibleUser(session.user);
     }
 
     @Transaction()
@@ -151,16 +172,28 @@ export class ShopAuthResolver extends BaseAuthResolver {
     async refreshCustomerVerification(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationRefreshCustomerVerificationArgs,
-    ) {
-        this.requireNativeAuthStrategy();
-        return this.customerService.refreshVerificationToken(ctx, args.emailAddress).then(() => true);
+    ): Promise<RefreshCustomerVerificationResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        await this.customerService.refreshVerificationToken(ctx, args.emailAddress);
+        return { success: true };
     }
 
     @Transaction()
     @Mutation()
     @Allow(Permission.Public)
-    async requestPasswordReset(@Ctx() ctx: RequestContext, @Args() args: MutationRequestPasswordResetArgs) {
-        return this.customerService.requestPasswordReset(ctx, args.emailAddress).then(() => true);
+    async requestPasswordReset(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationRequestPasswordResetArgs,
+    ): Promise<RequestPasswordResetResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        await this.customerService.requestPasswordReset(ctx, args.emailAddress);
+        return { success: true };
     }
 
     @Transaction()
@@ -171,27 +204,31 @@ export class ShopAuthResolver extends BaseAuthResolver {
         @Args() args: MutationResetPasswordArgs,
         @Context('req') req: Request,
         @Context('res') res: Response,
-    ) {
-        this.requireNativeAuthStrategy();
+    ): Promise<ResetPasswordResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
         const { token, password } = args;
-        const customer = await this.customerService.resetPassword(ctx, token, password);
-        if (customer && customer.user) {
-            return super.authenticateAndCreateSession(
-                ctx,
-                {
-                    input: {
-                        [NATIVE_AUTH_STRATEGY_NAME]: {
-                            username: customer.user.identifier,
-                            password: args.password,
-                        },
+        const result = await this.customerService.resetPassword(ctx, token, password);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+
+        const { user } = await super.authenticateAndCreateSession(
+            ctx,
+            {
+                input: {
+                    [NATIVE_AUTH_STRATEGY_NAME]: {
+                        username: result.identifier,
+                        password: args.password,
                     },
                 },
-                req,
-                res,
-            );
-        } else {
-            throw new PasswordResetTokenError();
-        }
+            },
+            req,
+            res,
+        );
+        return user;
     }
 
     @Transaction()
@@ -200,9 +237,15 @@ export class ShopAuthResolver extends BaseAuthResolver {
     async updateCustomerPassword(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdateCustomerPasswordArgs,
-    ): Promise<boolean> {
-        this.requireNativeAuthStrategy();
+    ): Promise<UpdateCustomerPasswordResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
         const result = await super.updatePassword(ctx, args.currentPassword, args.newPassword);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
         if (result && ctx.activeUserId) {
             const customer = await this.customerService.findOneByUserId(ctx, ctx.activeUserId);
             if (customer) {
@@ -214,7 +257,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
                 });
             }
         }
-        return result;
+        return { success: result };
     }
 
     @Transaction()
@@ -223,13 +266,29 @@ export class ShopAuthResolver extends BaseAuthResolver {
     async requestUpdateCustomerEmailAddress(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationRequestUpdateCustomerEmailAddressArgs,
-    ): Promise<boolean> {
-        this.requireNativeAuthStrategy();
+    ): Promise<RequestUpdateCustomerEmailAddressResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
         if (!ctx.activeUserId) {
             throw new ForbiddenError();
         }
-        await this.authService.verifyUserPassword(ctx, ctx.activeUserId, args.password);
-        return this.customerService.requestUpdateEmailAddress(ctx, ctx.activeUserId, args.newEmailAddress);
+        const verify = await this.authService.verifyUserPassword(ctx, ctx.activeUserId, args.password);
+        if (isGraphQlErrorResult(verify)) {
+            return verify;
+        }
+        const result = await this.customerService.requestUpdateEmailAddress(
+            ctx,
+            ctx.activeUserId,
+            args.newEmailAddress,
+        );
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        return {
+            success: result,
+        };
     }
 
     @Transaction()
@@ -238,12 +297,19 @@ export class ShopAuthResolver extends BaseAuthResolver {
     async updateCustomerEmailAddress(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdateCustomerEmailAddressArgs,
-    ): Promise<boolean> {
-        this.requireNativeAuthStrategy();
-        return this.customerService.updateEmailAddress(ctx, args.token);
+    ): Promise<UpdateCustomerEmailAddressResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        const result = await this.customerService.updateEmailAddress(ctx, args.token);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        return { success: result };
     }
 
-    private requireNativeAuthStrategy() {
+    private requireNativeAuthStrategy(): NativeAuthStrategyError | undefined {
         if (!this.nativeAuthStrategyIsConfigured) {
             const authStrategyNames = this.configService.authOptions.shopAuthenticationStrategy
                 .map(s => s.name)
@@ -252,7 +318,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
                 'This GraphQL operation requires that the NativeAuthenticationStrategy be configured for the Shop API.\n' +
                 `Currently the following AuthenticationStrategies are enabled: ${authStrategyNames}`;
             Logger.error(errorMessage);
-            throw new InternalServerError('error.');
+            return new NativeAuthStrategyError();
         }
     }
 }

+ 4 - 3
packages/core/src/api/resolvers/shop/shop-customer.resolver.ts

@@ -1,5 +1,5 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { MutationDeleteCustomerAddressArgs } from '@vendure/common/lib/generated-shop-types';
+import { MutationDeleteCustomerAddressArgs, Success } from '@vendure/common/lib/generated-shop-types';
 import {
     MutationCreateCustomerAddressArgs,
     MutationUpdateCustomerAddressArgs,
@@ -75,13 +75,14 @@ export class ShopCustomerResolver {
     async deleteCustomerAddress(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationDeleteCustomerAddressArgs,
-    ): Promise<boolean> {
+    ): Promise<Success> {
         const customer = await this.getCustomerForOwner(ctx);
         const customerAddresses = await this.customerService.findAddressesByCustomerId(ctx, customer.id);
         if (!customerAddresses.find(address => idsAreEqual(address.id, args.id))) {
             throw new ForbiddenError();
         }
-        return this.customerService.deleteAddress(ctx, args.id);
+        const success = await this.customerService.deleteAddress(ctx, args.id);
+        return { success };
     }
 
     /**

+ 35 - 12
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -1,5 +1,7 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    AddPaymentToOrderResult,
+    ApplyCouponCodeResult,
     MutationAddItemToOrderArgs,
     MutationAddPaymentToOrderArgs,
     MutationAdjustOrderLineArgs,
@@ -14,12 +16,19 @@ import {
     Permission,
     QueryOrderArgs,
     QueryOrderByCodeArgs,
+    RemoveOrderItemsResult,
+    SetCustomerForOrderResult,
+    SetOrderShippingMethodResult,
     ShippingMethodQuote,
+    TransitionOrderToStateResult,
+    UpdateOrderItemsResult,
 } from '@vendure/common/lib/generated-shop-types';
 import { QueryCountriesArgs } from '@vendure/common/lib/generated-types';
 import ms from 'ms';
 
-import { ForbiddenError, IllegalOperationError, InternalServerError } from '../../../common/error/errors';
+import { ErrorResultUnion, isGraphQlErrorResult } from '../../../common/error/error-result';
+import { ForbiddenError, InternalServerError } from '../../../common/error/errors';
+import { AlreadyLoggedInError } from '../../../common/error/generated-graphql-shop-errors';
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
 import { Country } from '../../../entity';
@@ -175,7 +184,7 @@ export class ShopOrderResolver {
     async setOrderShippingMethod(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationSetOrderShippingMethodArgs,
-    ): Promise<Order | undefined> {
+    ): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order> | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
             const sessionOrder = await this.getOrderFromContext(ctx);
             if (sessionOrder) {
@@ -215,10 +224,10 @@ export class ShopOrderResolver {
     async transitionOrderToState(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationTransitionOrderToStateArgs,
-    ): Promise<Order | undefined> {
+    ): Promise<ErrorResultUnion<TransitionOrderToStateResult, Order> | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
             const sessionOrder = await this.getOrderFromContext(ctx, true);
-            return this.orderService.transitionToState(ctx, sessionOrder.id, args.state as OrderState);
+            return await this.orderService.transitionToState(ctx, sessionOrder.id, args.state as OrderState);
         }
     }
 
@@ -228,7 +237,7 @@ export class ShopOrderResolver {
     async addItemToOrder(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationAddItemToOrderArgs,
-    ): Promise<Order> {
+    ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.getOrderFromContext(ctx, true);
         return this.orderService.addItemToOrder(
             ctx,
@@ -245,7 +254,7 @@ export class ShopOrderResolver {
     async adjustOrderLine(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationAdjustOrderLineArgs,
-    ): Promise<Order> {
+    ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         if (args.quantity === 0) {
             return this.removeOrderLine(ctx, { orderLineId: args.orderLineId });
         }
@@ -265,7 +274,7 @@ export class ShopOrderResolver {
     async removeOrderLine(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationRemoveOrderLineArgs,
-    ): Promise<Order> {
+    ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
         const order = await this.getOrderFromContext(ctx, true);
         return this.orderService.removeItemFromOrder(ctx, order.id, args.orderLineId);
     }
@@ -273,7 +282,9 @@ export class ShopOrderResolver {
     @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
-    async removeAllOrderLines(@Ctx() ctx: RequestContext): Promise<Order> {
+    async removeAllOrderLines(
+        @Ctx() ctx: RequestContext,
+    ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
         const order = await this.getOrderFromContext(ctx, true);
         return this.orderService.removeAllItemsFromOrder(ctx, order.id);
     }
@@ -284,7 +295,7 @@ export class ShopOrderResolver {
     async applyCouponCode(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationApplyCouponCodeArgs,
-    ): Promise<Order> {
+    ): Promise<ErrorResultUnion<ApplyCouponCodeResult, Order>> {
         const order = await this.getOrderFromContext(ctx, true);
         return this.orderService.applyCouponCode(ctx, order.id, args.couponCode);
     }
@@ -303,11 +314,17 @@ export class ShopOrderResolver {
     @Transaction()
     @Mutation()
     @Allow(Permission.UpdateOrder, Permission.Owner)
-    async addPaymentToOrder(@Ctx() ctx: RequestContext, @Args() args: MutationAddPaymentToOrderArgs) {
+    async addPaymentToOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationAddPaymentToOrderArgs,
+    ): Promise<ErrorResultUnion<AddPaymentToOrderResult, Order> | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
             const sessionOrder = await this.getOrderFromContext(ctx);
             if (sessionOrder) {
                 const order = await this.orderService.addPaymentToOrder(ctx, sessionOrder.id, args.input);
+                if (isGraphQlErrorResult(order)) {
+                    return order;
+                }
                 if (order.active === false) {
                     if (order.customer) {
                         const addresses = await this.customerService.findAddressesByCustomerId(
@@ -340,14 +357,20 @@ export class ShopOrderResolver {
     @Transaction()
     @Mutation()
     @Allow(Permission.Owner)
-    async setCustomerForOrder(@Ctx() ctx: RequestContext, @Args() args: MutationSetCustomerForOrderArgs) {
+    async setCustomerForOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationSetCustomerForOrderArgs,
+    ): Promise<ErrorResultUnion<SetCustomerForOrderResult, Order> | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
             if (ctx.activeUserId) {
-                throw new IllegalOperationError('error.cannot-set-customer-for-order-when-logged-in');
+                return new AlreadyLoggedInError();
             }
             const sessionOrder = await this.getOrderFromContext(ctx);
             if (sessionOrder) {
                 const customer = await this.customerService.createOrUpdate(ctx, args.input, true);
+                if (isGraphQlErrorResult(customer)) {
+                    return customer;
+                }
                 return this.orderService.addCustomerToOrder(ctx, sessionOrder.id, customer);
             }
         }

+ 1 - 1
packages/core/src/api/schema/admin-api/customer.api.graphql

@@ -20,7 +20,7 @@ type Mutation {
     updateCustomerAddress(input: UpdateAddressInput!): Address!
 
     "Update an existing Address"
-    deleteCustomerAddress(id: ID!): Boolean!
+    deleteCustomerAddress(id: ID!): Success!
 
     addNoteToCustomer(input: AddNoteToCustomerInput!): Customer!
     updateCustomerNote(input: UpdateCustomerNoteInput!): HistoryEntry!

+ 3 - 1
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -12,7 +12,7 @@ type Mutation {
     addNoteToOrder(input: AddNoteToOrderInput!): Order!
     updateOrderNote(input: UpdateOrderNoteInput!): HistoryEntry!
     deleteOrderNote(id: ID!): DeletionResponse!
-    transitionOrderToState(id: ID!, state: String!): Order
+    transitionOrderToState(id: ID!, state: String!): TransitionOrderToStateResult
     transitionFulfillmentToState(id: ID!, state: String!): Fulfillment!
     setOrderCustomFields(input: UpdateOrderInput!): Order
 }
@@ -72,3 +72,5 @@ input UpdateOrderNoteInput {
     note: String
     isPublic: Boolean
 }
+
+union TransitionOrderToStateResult = Order | OrderStateTransitionError

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

@@ -82,7 +82,7 @@ enum SortOrder {
 }
 
 enum ErrorCode {
-    UnknownError
+    UNKNOWN_ERROR
 }
 
 interface ErrorResult {
@@ -183,3 +183,19 @@ input UpdateAddressInput {
     defaultShippingAddress: Boolean
     defaultBillingAddress: Boolean
 }
+
+"""
+Indicates that an operation succeeded, where we do not want to return any more specific information.
+"""
+type Success {
+    success: Boolean!
+}
+
+"Returned if there is an error in transitioning the Order state"
+type OrderStateTransitionError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    transitionError: String!
+    fromState: String!
+    toState: String!
+}

+ 215 - 19
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -43,19 +43,19 @@ type Query {
 
 type Mutation {
     "Adds an item to the order. If custom fields are defined on the OrderLine entity, a third argument 'customFields' will be available."
-    addItemToOrder(productVariantId: ID!, quantity: Int!): Order
+    addItemToOrder(productVariantId: ID!, quantity: Int!): UpdateOrderItemsResult!
     "Remove an OrderLine from the Order"
-    removeOrderLine(orderLineId: ID!): Order
+    removeOrderLine(orderLineId: ID!): RemoveOrderItemsResult!
     "Remove all OrderLine from the Order"
-    removeAllOrderLines: Order
+    removeAllOrderLines: RemoveOrderItemsResult!
     "Adjusts an OrderLine. If custom fields are defined on the OrderLine entity, a third argument 'customFields' of type `OrderLineCustomFieldsInput` will be available."
-    adjustOrderLine(orderLineId: ID!, quantity: Int): Order
+    adjustOrderLine(orderLineId: ID!, quantity: Int): UpdateOrderItemsResult!
     "Applies the given coupon code to the active Order"
-    applyCouponCode(couponCode: String!): Order
+    applyCouponCode(couponCode: String!): ApplyCouponCodeResult!
     "Removes the given coupon code from the active Order"
     removeCouponCode(couponCode: String!): Order
     "Transitions an Order to a new state. Valid next states can be found by querying `nextOrderStates`"
-    transitionOrderToState(state: String!): Order
+    transitionOrderToState(state: String!): TransitionOrderToStateResult
     "Sets the shipping address for this order"
     setOrderShippingAddress(input: CreateAddressInput!): Order
     "Sets the billing address for this order"
@@ -63,19 +63,17 @@ type Mutation {
     "Allows any custom fields to be set for the active order"
     setOrderCustomFields(input: UpdateOrderInput!): Order
     "Sets the shipping method by id, which can be obtained with the `eligibleShippingMethods` query"
-    setOrderShippingMethod(shippingMethodId: ID!): Order
+    setOrderShippingMethod(shippingMethodId: ID!): SetOrderShippingMethodResult!
     "Add a Payment to the Order"
-    addPaymentToOrder(input: PaymentInput!): Order
+    addPaymentToOrder(input: PaymentInput!): AddPaymentToOrderResult
     "Set the Customer for the Order. Required only if the Customer is not currently logged in"
-    setCustomerForOrder(input: CreateCustomerInput!): Order
+    setCustomerForOrder(input: CreateCustomerInput!): SetCustomerForOrderResult
     "Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})`"
     login(username: String!, password: String!, rememberMe: Boolean): LoginResult!
     "Authenticates the user using a named authentication strategy"
     authenticate(input: AuthenticationInput!, rememberMe: Boolean): LoginResult!
     "End the current authenticated session"
     logout: Boolean!
-    "Regenerate and send a verification token for a new Customer registration. Only applicable if `authOptions.requireVerification` is set to true."
-    refreshCustomerVerification(emailAddress: String!): Boolean!
     """
     Register a Customer account with the given credentials. There are three possible registration flows:
 
@@ -92,7 +90,9 @@ type Mutation {
 
     3. The Customer _must_ be registered _with_ a password. No further action is needed - the Customer is able to authenticate immediately.
     """
-    registerCustomerAccount(input: RegisterCustomerInput!): Boolean!
+    registerCustomerAccount(input: RegisterCustomerInput!): RegisterCustomerAccountResult!
+    "Regenerate and send a verification token for a new Customer registration. Only applicable if `authOptions.requireVerification` is set to true."
+    refreshCustomerVerification(emailAddress: String!): RefreshCustomerVerificationResult!
     "Update an existing Customer"
     updateCustomer(input: UpdateCustomerInput!): Customer!
     "Create a new Customer Address"
@@ -100,32 +100,35 @@ type Mutation {
     "Update an existing Address"
     updateCustomerAddress(input: UpdateAddressInput!): Address!
     "Delete an existing Address"
-    deleteCustomerAddress(id: ID!): Boolean!
+    deleteCustomerAddress(id: ID!): Success!
     """
     Verify a Customer email address with the token sent to that address. Only applicable if `authOptions.requireVerification` is set to true.
 
     If the Customer was not registered with a password in the `registerCustomerAccount` mutation, the a password _must_ be
     provided here.
     """
-    verifyCustomerAccount(token: String!, password: String): LoginResult!
+    verifyCustomerAccount(token: String!, password: String): VerifyCustomerAccountResult!
     "Update the password of the active Customer"
-    updateCustomerPassword(currentPassword: String!, newPassword: String!): Boolean
+    updateCustomerPassword(currentPassword: String!, newPassword: String!): UpdateCustomerPasswordResult!
     """
     Request to update the emailAddress of the active Customer. If `authOptions.requireVerification` is enabled
     (as is the default), then the `identifierChangeToken` will be assigned to the current User and
     a IdentifierChangeRequestEvent will be raised. This can then be used e.g. by the EmailPlugin to email
     that verification token to the Customer, which is then used to verify the change of email address.
     """
-    requestUpdateCustomerEmailAddress(password: String!, newEmailAddress: String!): Boolean
+    requestUpdateCustomerEmailAddress(
+        password: String!
+        newEmailAddress: String!
+    ): RequestUpdateCustomerEmailAddressResult!
     """
     Confirm the update of the emailAddress with the provided token, which has been generated by the
     `requestUpdateCustomerEmailAddress` mutation.
     """
-    updateCustomerEmailAddress(token: String!): Boolean
+    updateCustomerEmailAddress(token: String!): UpdateCustomerEmailAddressResult!
     "Requests a password reset email to be sent"
-    requestPasswordReset(emailAddress: String!): Boolean
+    requestPasswordReset(emailAddress: String!): RequestPasswordResetResult
     "Resets a Customer's password based on the provided token"
-    resetPassword(token: String!, password: String!): LoginResult!
+    resetPassword(token: String!, password: String!): ResetPasswordResult!
 }
 
 # Populated at run-time
@@ -174,3 +177,196 @@ input OrderListOptions
 
 # generated by generateListOptions function
 input ProductListOptions
+
+"Returned when attempting to modify the contents of an Order that is not in the `AddingItems` state."
+type OrderModificationError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Retured when the maximum order size limit has been reached."
+type OrderLimitError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    maxItems: Int!
+}
+
+"Retured when attemting to set a negative OrderLine quantity."
+type NegativeQuantityError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned when attempting to add a Payment to an Order that is not in the `ArrangingPayment` state."
+type OrderPaymentStateError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned when a Payment fails due to an error."
+type PaymentFailedError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    paymentErrorMessage: String!
+}
+
+"Returned when a Payment is declined by the payment provider."
+type PaymentDeclinedError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    paymentErrorMessage: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeInvalidError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    couponCode: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeInvalidError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    couponCode: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeExpiredError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    couponCode: String!
+}
+
+"Returned if the provided coupon code is invalid"
+type CouponCodeLimitError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    couponCode: String!
+    limit: Int!
+}
+
+"Retured when attemting to set the Customer for an Order when already logged in."
+type AlreadyLoggedInError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Retured when attemting to create a Customer with an email address already registered to an existing User."
+type EmailAddressConflictError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Retured when attemting to register or verify a customer account without a password, when one is required."
+type MissingPasswordError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Retured when attemting to verify a customer account with a password, when a password has already been set."
+type PasswordAlreadySetError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Retured when attempting an operation that relies on the NativeAuthStrategy, if that strategy is not configured."
+type NativeAuthStrategyError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Retured if the verification token (used to verify a Customer's email address) is either
+invalid or does not match any expected tokens.
+"""
+type VerificationTokenInvalidError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Returned if the verification token (used to verify a Customer's email address) is valid, but has
+expired according to the `verificationTokenDuration` setting in the AuthOptions.
+"""
+type VerificationTokenExpiredError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Retured if the token used to change a Customer's email address is either
+invalid or does not match any expected tokens.
+"""
+type IdentifierChangeTokenInvalidError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Retured if the token used to change a Customer's email address is valid, but has
+expired according to the `verificationTokenDuration` setting in the AuthOptions.
+"""
+type IdentifierChangeTokenExpiredError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if the user credentials are not valid"
+type InvalidCredentialsError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Retured if the token used to reset a Customer's password is either
+invalid or does not match any expected tokens.
+"""
+type PasswordResetTokenInvalidError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"""
+Retured if the token used to reset a Customer's password is valid, but has
+expired according to the `verificationTokenDuration` setting in the AuthOptions.
+"""
+type PasswordResetTokenExpiredError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+union UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError
+union RemoveOrderItemsResult = Order | OrderModificationError
+union SetOrderShippingMethodResult = Order | OrderModificationError
+union ApplyCouponCodeResult = Order | CouponCodeExpiredError | CouponCodeInvalidError | CouponCodeLimitError
+union AddPaymentToOrderResult =
+      Order
+    | OrderPaymentStateError
+    | PaymentFailedError
+    | PaymentDeclinedError
+    | OrderStateTransitionError
+union TransitionOrderToStateResult = Order | OrderStateTransitionError
+union SetCustomerForOrderResult = Order | AlreadyLoggedInError | EmailAddressConflictError
+union RegisterCustomerAccountResult = Success | MissingPasswordError | NativeAuthStrategyError
+union RefreshCustomerVerificationResult = Success | NativeAuthStrategyError
+union VerifyCustomerAccountResult =
+      CurrentUser
+    | VerificationTokenInvalidError
+    | VerificationTokenExpiredError
+    | MissingPasswordError
+    | PasswordAlreadySetError
+    | NativeAuthStrategyError
+union UpdateCustomerPasswordResult = Success | InvalidCredentialsError | NativeAuthStrategyError
+union RequestUpdateCustomerEmailAddressResult =
+      Success
+    | InvalidCredentialsError
+    | EmailAddressConflictError
+    | NativeAuthStrategyError
+union UpdateCustomerEmailAddressResult =
+      Success
+    | IdentifierChangeTokenInvalidError
+    | IdentifierChangeTokenExpiredError
+    | NativeAuthStrategyError
+union RequestPasswordResetResult = Success | NativeAuthStrategyError
+union ResetPasswordResult = CurrentUser | PasswordResetTokenInvalidError | PasswordResetTokenExpiredError | NativeAuthStrategyError

+ 56 - 0
packages/core/src/common/error/error-result.ts

@@ -0,0 +1,56 @@
+import { ErrorResult as GraphQLErrorResultShop } from '@vendure/common/lib/generated-shop-types';
+import { ErrorResult as GraphQLErrorResultAdmin } from '@vendure/common/lib/generated-types';
+
+import { VendureEntity } from '../../entity/base/base.entity';
+
+export type GraphQLErrorResult = GraphQLErrorResultShop | GraphQLErrorResultAdmin;
+
+/**
+ * @description
+ * Takes an ErrorResult union type (i.e. a generated union type consisting of some query/mutation result
+ * plus one or more ErrorResult types) and returns a union of _just_ the ErrorResult types.
+ *
+ * @example
+ * ```TypeScript
+ * type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError;
+ *
+ * type T1 = JustErrorResults<UpdateOrderItemsResult>;
+ * // T1 = OrderModificationError | OrderLimitError | NegativeQuantityError
+ * ```
+ */
+export type JustErrorResults<T extends GraphQLErrorResult | U, U = any> = Exclude<
+    T,
+    T extends GraphQLErrorResult ? never : T
+>;
+
+/**
+ * @description
+ * Used to construct a TypeScript return type for a query or mutation which, in the GraphQL schema,
+ * returns a union type composed of a success result (e.g. Order) plus one or more ErrorResult
+ * types.
+ *
+ * Since the TypeScript entities do not correspond 1-to-1 with their GraphQL type counterparts,
+ * we use this type to substitute them.
+ *
+ * @example
+ * ```TypeScript
+ * type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError;
+ * type T1 = ErrorResultUnion<UpdateOrderItemsResult, VendureEntityOrder>;
+ * // T1 = VendureEntityOrder | OrderModificationError | OrderLimitError | NegativeQuantityError;
+ */
+export type ErrorResultUnion<T extends GraphQLErrorResult | U, E extends VendureEntity, U = any> =
+    | JustErrorResults<T>
+    | E;
+
+/**
+ * @description
+ * Returns true if the ErrorResultUnion is actually an ErrorResult type.
+ */
+export function isGraphQlErrorResult<T extends GraphQLErrorResult | U, U = any>(
+    input: T,
+): input is JustErrorResults<T>;
+export function isGraphQlErrorResult<T, E extends VendureEntity>(
+    input: ErrorResultUnion<T, E>,
+): input is JustErrorResults<ErrorResultUnion<T, E>> {
+    return !!((input as any).code && (input as any).message != null) && (input as any).__typename;
+}

+ 0 - 152
packages/core/src/common/error/errors.ts

@@ -98,100 +98,6 @@ export class EntityNotFoundError extends I18nError {
     }
 }
 
-/**
- * @description
- * This error should be thrown when the verification token (used to verify a Customer's email
- * address) is either invalid or does not match any expected tokens.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class VerificationTokenError extends I18nError {
-    constructor() {
-        super('error.verification-token-not-recognized', {}, 'BAD_VERIFICATION_TOKEN', LogLevel.Warn);
-    }
-}
-
-/**
- * @description
- * This error should be thrown when the verification token (used to verify a Customer's email
- * address) is valid, but has expired according to the `verificationTokenDuration` setting
- * in {@link AuthOptions}.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class VerificationTokenExpiredError extends I18nError {
-    constructor() {
-        super('error.verification-token-has-expired', {}, 'EXPIRED_VERIFICATION_TOKEN', LogLevel.Warn);
-    }
-}
-
-/**
- * @description
- * This error should be thrown when an error occurs trying to reset a Customer's password.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class PasswordResetTokenError extends I18nError {
-    constructor() {
-        super('error.password-reset-token-not-recognized', {}, 'BAD_PASSWORD_RESET_TOKEN', LogLevel.Warn);
-    }
-}
-
-/**
- * @description
- * This error should be thrown when an error occurs trying to reset a Customer's password
- * by reason of the token having expired.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class PasswordResetTokenExpiredError extends I18nError {
-    constructor() {
-        super('error.password-reset-token-has-expired', {}, 'EXPIRED_PASSWORD_RESET_TOKEN', LogLevel.Warn);
-    }
-}
-
-/**
- * @description
- * This error should be thrown when an error occurs when attempting to update a User's identifier
- * (e.g. change email address).
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class IdentifierChangeTokenError extends I18nError {
-    constructor() {
-        super(
-            'error.identifier-change-token-not-recognized',
-            {},
-            'EXPIRED_IDENTIFIER_CHANGE_TOKEN',
-            LogLevel.Warn,
-        );
-    }
-}
-
-/**
- * @description
- * This error should be thrown when an error occurs when attempting to update a User's identifier
- * (e.g. change email address) by reason of the token having expired.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class IdentifierChangeTokenExpiredError extends I18nError {
-    constructor() {
-        super(
-            'error.identifier-change-token-has-expired',
-            {},
-            'EXPIRED_IDENTIFIER_CHANGE_TOKEN',
-            LogLevel.Warn,
-        );
-    }
-}
-
 /**
  * @description
  * This error should be thrown when the `requireVerification` in {@link AuthOptions} is set to
@@ -205,61 +111,3 @@ export class NotVerifiedError extends I18nError {
         super('error.email-address-not-verified', {}, 'NOT_VERIFIED', LogLevel.Warn);
     }
 }
-
-/**
- * @description
- * This error should be thrown when the number or OrderItems in an Order exceeds the limit
- * specified by the `orderItemsLimit` setting in {@link OrderOptions}.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class OrderItemsLimitError extends I18nError {
-    constructor(maxItems: number) {
-        super('error.order-items-limit-exceeded', { maxItems }, 'ORDER_ITEMS_LIMIT_EXCEEDED', LogLevel.Warn);
-    }
-}
-
-/**
- * @description
- * This error is thrown when the coupon code is not associated with any active Promotion.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class CouponCodeInvalidError extends I18nError {
-    constructor(couponCode: string) {
-        super('error.coupon-code-not-valid', { couponCode }, 'COUPON_CODE_INVALID', LogLevel.Verbose);
-    }
-}
-
-/**
- * @description
- * This error is thrown when the coupon code is associated with a Promotion that has expired.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class CouponCodeExpiredError extends I18nError {
-    constructor(couponCode: string) {
-        super('error.coupon-code-expired', { couponCode }, 'COUPON_CODE_EXPIRED', LogLevel.Verbose);
-    }
-}
-
-/**
- * @description
- * This error is thrown when the coupon code is associated with a Promotion that has expired.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class CouponCodeLimitError extends I18nError {
-    constructor(limit: number) {
-        super(
-            'error.coupon-code-limit-has-been-reached',
-            { limit },
-            'COUPON_CODE_LIMIT_REACHED',
-            LogLevel.Verbose,
-        );
-    }
-}

+ 47 - 30
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -4,45 +4,62 @@
 import { ErrorCode } from '@vendure/common/lib/generated-types';
 
 export type Scalars = {
-    ID: string;
-    String: string;
-    Boolean: boolean;
-    Int: number;
-    Float: number;
-    DateTime: any;
-    JSON: any;
-    Upload: any;
+  ID: string;
+  String: string;
+  Boolean: boolean;
+  Int: number;
+  Float: number;
+  DateTime: any;
+  JSON: any;
+  Upload: any;
 };
 
 export class ErrorResult {
-    readonly __typename: string;
-    readonly code: ErrorCode;
-    message: Scalars['String'];
+  readonly __typename: string;
+  readonly code: ErrorCode;
+  message: Scalars['String'];
 }
 
 export class MimeTypeError extends ErrorResult {
-    readonly __typename = 'MimeTypeError';
-    readonly code = ErrorCode.MimeTypeError;
-    constructor(
-        public message: Scalars['String'],
-        public fileName: Scalars['String'],
-        public mimeType: Scalars['String'],
-    ) {
-        super();
-    }
+  readonly __typename = 'MimeTypeError';
+  readonly code = ErrorCode.MIME_TYPE_ERROR;
+  readonly message = 'MIME_TYPE_ERROR';
+  constructor(
+    public   fileName: Scalars['String'],
+    public   mimeType: Scalars['String'],
+  ) {
+    super();
+  }
 }
 
-const errorTypeNames = new Set(['MimeTypeError']);
-export function isGraphQLError(
-    input: any,
-): input is import('@vendure/common/lib/generated-types').ErrorResult {
-    return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
+export class OrderStateTransitionError extends ErrorResult {
+  readonly __typename = 'OrderStateTransitionError';
+  readonly code = ErrorCode.ORDER_STATE_TRANSITION_ERROR;
+  readonly message = 'ORDER_STATE_TRANSITION_ERROR';
+  constructor(
+    public   transitionError: Scalars['String'],
+    public   fromState: Scalars['String'],
+    public   toState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+
+const errorTypeNames = new Set(['MimeTypeError', 'OrderStateTransitionError']);
+function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
+  return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }
 
 export const adminErrorOperationTypeResolvers = {
-    CreateAssetResult: {
-        __resolveType(value: any) {
-            return isGraphQLError(value) ? (value as any).__typename : 'Asset';
-        },
+  CreateAssetResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Asset';
     },
-};
+  },
+  TransitionOrderToStateResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+};

+ 328 - 10
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -1,17 +1,335 @@
 // tslint:disable
 /** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */
 
-import { ErrorCode } from '@vendure/common/lib/generated-types';
+import { ErrorCode } from '@vendure/common/lib/generated-shop-types';
 
 export type Scalars = {
-    ID: string;
-    String: string;
-    Boolean: boolean;
-    Int: number;
-    Float: number;
-    DateTime: any;
-    JSON: any;
-    Upload: any;
+  ID: string;
+  String: string;
+  Boolean: boolean;
+  Int: number;
+  Float: number;
+  DateTime: any;
+  JSON: any;
+  Upload: any;
 };
 
-export const shopErrorOperationTypeResolvers = {};
+export class ErrorResult {
+  readonly __typename: string;
+  readonly code: ErrorCode;
+  message: Scalars['String'];
+}
+
+export class AlreadyLoggedInError extends ErrorResult {
+  readonly __typename = 'AlreadyLoggedInError';
+  readonly code = ErrorCode.ALREADY_LOGGED_IN_ERROR;
+  readonly message = 'ALREADY_LOGGED_IN_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class CouponCodeExpiredError extends ErrorResult {
+  readonly __typename = 'CouponCodeExpiredError';
+  readonly code = ErrorCode.COUPON_CODE_EXPIRED_ERROR;
+  readonly message = 'COUPON_CODE_EXPIRED_ERROR';
+  constructor(
+    public   couponCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class CouponCodeInvalidError extends ErrorResult {
+  readonly __typename = 'CouponCodeInvalidError';
+  readonly code = ErrorCode.COUPON_CODE_INVALID_ERROR;
+  readonly message = 'COUPON_CODE_INVALID_ERROR';
+  constructor(
+    public   couponCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class CouponCodeLimitError extends ErrorResult {
+  readonly __typename = 'CouponCodeLimitError';
+  readonly code = ErrorCode.COUPON_CODE_LIMIT_ERROR;
+  readonly message = 'COUPON_CODE_LIMIT_ERROR';
+  constructor(
+    public   couponCode: Scalars['String'],
+    public   limit: Scalars['Int'],
+  ) {
+    super();
+  }
+}
+
+export class EmailAddressConflictError extends ErrorResult {
+  readonly __typename = 'EmailAddressConflictError';
+  readonly code = ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR;
+  readonly message = 'EMAIL_ADDRESS_CONFLICT_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class IdentifierChangeTokenExpiredError extends ErrorResult {
+  readonly __typename = 'IdentifierChangeTokenExpiredError';
+  readonly code = ErrorCode.IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR;
+  readonly message = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class IdentifierChangeTokenInvalidError extends ErrorResult {
+  readonly __typename = 'IdentifierChangeTokenInvalidError';
+  readonly code = ErrorCode.IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR;
+  readonly message = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class InvalidCredentialsError extends ErrorResult {
+  readonly __typename = 'InvalidCredentialsError';
+  readonly code = ErrorCode.INVALID_CREDENTIALS_ERROR;
+  readonly message = 'INVALID_CREDENTIALS_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class MissingPasswordError extends ErrorResult {
+  readonly __typename = 'MissingPasswordError';
+  readonly code = ErrorCode.MISSING_PASSWORD_ERROR;
+  readonly message = 'MISSING_PASSWORD_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class NativeAuthStrategyError extends ErrorResult {
+  readonly __typename = 'NativeAuthStrategyError';
+  readonly code = ErrorCode.NATIVE_AUTH_STRATEGY_ERROR;
+  readonly message = 'NATIVE_AUTH_STRATEGY_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class NegativeQuantityError extends ErrorResult {
+  readonly __typename = 'NegativeQuantityError';
+  readonly code = ErrorCode.NEGATIVE_QUANTITY_ERROR;
+  readonly message = 'NEGATIVE_QUANTITY_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class OrderLimitError extends ErrorResult {
+  readonly __typename = 'OrderLimitError';
+  readonly code = ErrorCode.ORDER_LIMIT_ERROR;
+  readonly message = 'ORDER_LIMIT_ERROR';
+  constructor(
+    public   maxItems: Scalars['Int'],
+  ) {
+    super();
+  }
+}
+
+export class OrderModificationError extends ErrorResult {
+  readonly __typename = 'OrderModificationError';
+  readonly code = ErrorCode.ORDER_MODIFICATION_ERROR;
+  readonly message = 'ORDER_MODIFICATION_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class OrderPaymentStateError extends ErrorResult {
+  readonly __typename = 'OrderPaymentStateError';
+  readonly code = ErrorCode.ORDER_PAYMENT_STATE_ERROR;
+  readonly message = 'ORDER_PAYMENT_STATE_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class OrderStateTransitionError extends ErrorResult {
+  readonly __typename = 'OrderStateTransitionError';
+  readonly code = ErrorCode.ORDER_STATE_TRANSITION_ERROR;
+  readonly message = 'ORDER_STATE_TRANSITION_ERROR';
+  constructor(
+    public   transitionError: Scalars['String'],
+    public   fromState: Scalars['String'],
+    public   toState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class PasswordAlreadySetError extends ErrorResult {
+  readonly __typename = 'PasswordAlreadySetError';
+  readonly code = ErrorCode.PASSWORD_ALREADY_SET_ERROR;
+  readonly message = 'PASSWORD_ALREADY_SET_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class PasswordResetTokenExpiredError extends ErrorResult {
+  readonly __typename = 'PasswordResetTokenExpiredError';
+  readonly code = ErrorCode.PASSWORD_RESET_TOKEN_EXPIRED_ERROR;
+  readonly message = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class PasswordResetTokenInvalidError extends ErrorResult {
+  readonly __typename = 'PasswordResetTokenInvalidError';
+  readonly code = ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR;
+  readonly message = 'PASSWORD_RESET_TOKEN_INVALID_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class PaymentDeclinedError extends ErrorResult {
+  readonly __typename = 'PaymentDeclinedError';
+  readonly code = ErrorCode.PAYMENT_DECLINED_ERROR;
+  readonly message = 'PAYMENT_DECLINED_ERROR';
+  constructor(
+    public   paymentErrorMessage: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class PaymentFailedError extends ErrorResult {
+  readonly __typename = 'PaymentFailedError';
+  readonly code = ErrorCode.PAYMENT_FAILED_ERROR;
+  readonly message = 'PAYMENT_FAILED_ERROR';
+  constructor(
+    public   paymentErrorMessage: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class VerificationTokenExpiredError extends ErrorResult {
+  readonly __typename = 'VerificationTokenExpiredError';
+  readonly code = ErrorCode.VERIFICATION_TOKEN_EXPIRED_ERROR;
+  readonly message = 'VERIFICATION_TOKEN_EXPIRED_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class VerificationTokenInvalidError extends ErrorResult {
+  readonly __typename = 'VerificationTokenInvalidError';
+  readonly code = ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR;
+  readonly message = 'VERIFICATION_TOKEN_INVALID_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+
+const errorTypeNames = new Set(['OrderModificationError', 'OrderLimitError', 'NegativeQuantityError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'OrderStateTransitionError', 'OrderPaymentStateError', 'PaymentFailedError', 'PaymentDeclinedError', 'AlreadyLoggedInError', 'EmailAddressConflictError', 'MissingPasswordError', 'NativeAuthStrategyError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'PasswordAlreadySetError', 'InvalidCredentialsError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError']);
+function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
+  return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
+}
+
+export const shopErrorOperationTypeResolvers = {
+  UpdateOrderItemsResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  RemoveOrderItemsResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  ApplyCouponCodeResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  TransitionOrderToStateResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  SetOrderShippingMethodResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  AddPaymentToOrderResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  SetCustomerForOrderResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  RegisterCustomerAccountResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  RefreshCustomerVerificationResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  VerifyCustomerAccountResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
+  UpdateCustomerPasswordResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  RequestUpdateCustomerEmailAddressResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  UpdateCustomerEmailAddressResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  RequestPasswordResetResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Success';
+    },
+  },
+  ResetPasswordResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
+};

+ 1 - 1
packages/core/src/common/types/common-types.ts

@@ -51,7 +51,7 @@ export interface ListQueryOptions<T extends VendureEntity> {
  * nullable fields have the type `field?: <type> | null`.
  */
 export type NullOptionals<T> = {
-    [K in keyof T]: undefined extends T[K] ? NullOptionals<T[K]> | null : NullOptionals<T[K]>
+    [K in keyof T]: undefined extends T[K] ? NullOptionals<T[K]> | null : NullOptionals<T[K]>;
 };
 
 export type SortOrder = 'ASC' | 'DESC';

+ 1 - 1
packages/core/src/common/utils.ts

@@ -14,7 +14,7 @@ export function not(predicate: (...args: any[]) => boolean) {
  * as determined by a === equality check on the given compareBy property.
  */
 export function foundIn<T>(set: T[], compareBy: keyof T) {
-    return (item: T) => set.some((t) => t[compareBy] === item[compareBy]);
+    return (item: T) => set.some(t => t[compareBy] === item[compareBy]);
 }
 
 /**

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

@@ -29,8 +29,24 @@ export type OnPaymentTransitionStartReturnType = ReturnType<
 export interface CreatePaymentResult {
     amount: number;
     state: Exclude<PaymentState, 'Refunded' | 'Error'>;
+    /**
+     * @description
+     * The unique payment reference code typically assigned by
+     * the payment provider.
+     */
     transactionId?: string;
+    /**
+     * @description
+     * If the payment is declined or fails for ome other reason, pass the
+     * relevant error message here, and it gets returned with the
+     * ErrorResponse of the `addPaymentToOrder` mutation.
+     */
     errorMessage?: string;
+    /**
+     * @description
+     * This field can be used to store other relevant data which is often
+     * provided by the payment provider.
+     */
     metadata?: PaymentMetadata;
 }
 

+ 16 - 0
packages/core/src/i18n/i18n.service.ts

@@ -7,6 +7,7 @@ import ICU from 'i18next-icu';
 import Backend from 'i18next-node-fs-backend';
 import path from 'path';
 
+import { GraphQLErrorResult } from '../common/error/error-result';
 import { ConfigService } from '../config/config.service';
 
 import { I18nError } from './i18n-error';
@@ -69,4 +70,19 @@ export class I18nService implements OnModuleInit {
 
         return error;
     }
+
+    /**
+     * Translates the message of an ErrorResult
+     */
+    translateErrorResult(req: I18nRequest, error: GraphQLErrorResult) {
+        const t: TFunction = req.t;
+        let translation: string = error.message;
+        const key = `errorResult.${error.message}`;
+        try {
+            translation = t(key, error as any);
+        } catch (e) {
+            translation += ` (Translation format error: ${e.message})`;
+        }
+        error.message = translation;
+    }
 }

+ 37 - 25
packages/core/src/i18n/messages/en.json

@@ -10,34 +10,19 @@
     "cannot-move-collection-into-self": "Cannot move a Collection into itself",
     "cannot-remove-option-group-due-to-variants": "Cannot remove ProductOptionGroup \"{ code }\" as it is used by {count, plural, one {1 ProductVariant} other {# ProductVariants}}",
     "cannot-remove-tax-category-due-to-tax-rates": "Cannot remove TaxCategory \"{ name }\" as it is referenced by {count, plural, one {1 TaxRate} other {# TaxRates}}",
-    "cannot-set-customer-for-order-when-logged-in": "Cannot set a Customer for the Order when already logged in",
     "cannot-set-default-language-as-unavailable": "Cannot remove make language \"{ language }\" unavailable as it is used as the defaultLanguage by the channel \"{channelCode}\"",
-    "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-payment-from-to": "Cannot transition Payment from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-refund-from-to": "Cannot transition Refund from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-fulfillment-from-to": "Cannot transition Fulfillment from \"{ fromState }\" to \"{ toState }\"",
-    "cannot-transition-to-shipping-when-order-is-empty": "Cannot transition Order to the \"ArrangingShipping\" state when it is empty",
-    "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",
-    "cannot-transition-unless-all-cancelled": "Cannot transition Order to the \"Cancelled\" state unless all OrderItems are cancelled",
-    "cannot-transition-unless-all-order-items-delivered": "Cannot transition Order to the \"Delivered\" state unless all OrderItems are delivered",
-    "cannot-transition-unless-some-order-items-delivered": "Cannot transition Order to the \"PartiallyDelivered\" state unless some OrderItems are delivered",
-    "cannot-transition-unless-some-order-items-shipped": "Cannot transition Order to the \"PartiallyShipped\" state unless some OrderItems are shipped",
-    "cannot-transition-unless-all-order-items-shipped": "Cannot transition Order to the \"Shipped\" state unless all OrderItems are shipped",
-    "cannot-transition-without-authorized-payments": "Cannot transition Order to the \"PaymentAuthorized\" state when the total is not covered by authorized Payments",
-    "cannot-transition-without-settled-payments": "Cannot transition Order to the \"PaymentSettled\" state when the total is not covered by settled Payments",
-    "cannot-use-registered-email-address-for-guest-order":  "Cannot use a registered email address for a guest order. Please log in first",
-    "channel-not-found":  "No channel with the token \"{ token }\" exists",
+    "channel-not-found": "No channel with the token \"{ token }\" exists",
     "collection-id-or-slug-must-be-provided": "Either the Collection id or slug must be provided",
     "collection-id-slug-mismatch": "The provided id and slug refer to different Collections",
-    "country-code-not-valid":  "The countryCode \"{ countryCode }\" was not recognized",
-    "coupon-code-expired":  "Coupon code \"{ couponCode }\" has expired",
-    "coupon-code-limit-has-been-reached":  "Coupon code cannot be used more than {limit, plural, one {once} other {# times}} per customer",
-    "coupon-code-not-valid":  "Coupon code \"{ couponCode }\" is not valid",
+    "country-code-not-valid": "The countryCode \"{ countryCode }\" was not recognized",
     "create-fulfillment-items-already-fulfilled": "One or more OrderItems have already been fulfilled",
     "create-fulfillment-orders-must-be-settled": "One or more OrderItems belong to an Order which is in an invalid state",
     "create-fulfillment-nothing-to-fulfill": "Nothing to fulfill",
     "customer-does-not-belong-to-customer-group": "Customer does not belong to this CustomerGroup",
-    "default-channel-not-found":  "Default channel not found",
+    "default-channel-not-found": "Default channel not found",
     "email-address-must-be-unique": "The email address must be unique",
     "email-address-not-available": "This email address is not available",
     "email-address-not-verified": "Please verify this email address before logging in",
@@ -52,25 +37,19 @@
     "field-invalid-string-option": "The custom field '{ name }' value ['{ value }'] is invalid. Valid options are [{ validOptions }]",
     "field-invalid-string-pattern": "The custom field '{ name }' value ['{ value }'] does not match the pattern [{ pattern }]",
     "forbidden": "You are not currently authorized to perform this action",
-    "identifier-change-token-not-recognized": "Identifier change token not recognized",
     "identifier-change-token-has-expired": "Identifier change token has expired",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
     "language-not-available-in-global-settings": "Language \"{code}\" is not available. First enable it via GlobalSettings and try again.",
-    "mime-type-not-permitted": "The MIME type '{ mimetype }' is not permitted.",
-    "missing-password-on-registration": "A password must be provided when `authOptions.requireVerification` is set to \"false\"",
     "no-active-tax-zone": "The active tax zone could not be determined. Ensure a default tax zone is set for the current channel.",
     "no-search-plugin-configured": "No search plugin has been configured",
     "no-valid-channel-specified": "No valid channel was specified (ensure the 'vendure-token' header was specified in the request)",
-    "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
-    "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem",
-    "order-items-limit-exceeded": "Cannot add items. An order may consist of a maximum of { maxItems } items",
     "order-lines-must-belong-to-same-order": "OrderLines must all belong to a single Order",
-    "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
     "password-already-set-during-registration": "A password has already been set during registration",
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-not-recognized": "Password reset token not recognized",
     "password-required-for-verification": "A password must be provided as it was not set during registration",
+    "pending-identifier-missing": "Could not find the pending email address to update",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
     "products-cannot-be-removed-from-default-channel": "Products cannot be removed from the default Channel",
     "product-id-or-slug-must-be-provided": "Either the Product id or slug must be provided",
@@ -88,8 +67,41 @@
     "verification-token-not-recognized": "Verification token not recognized",
     "unauthorized": "The credentials did not match. Please check and try again"
   },
+  "errorResult": {
+    "ALREADY_LOGGED_IN_ERROR": "Cannot set a Customer for the Order when already logged in",
+    "COUPON_CODE_EXPIRED_ERROR": "Coupon code \"{ couponCode }\" has expired",
+    "COUPON_CODE_INVALID_ERROR": "Coupon code \"{ couponCode }\" is not valid",
+    "COUPON_CODE_LIMIT_ERROR": "Coupon code cannot be used more than {limit, plural, one {once} other {# times}} per customer",
+    "EMAIL_ADDRESS_CONFLICT_ERROR": "The email address is not available.",
+    "IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR": "Identifier change token not recognized",
+    "INVALID_CREDENTIALS_ERROR": "The provided credentials are invalid",
+    "MIME_TYPE_ERROR": "The MIME type '{ mimeType }' is not permitted.",
+    "MISSING_PASSWORD_ERROR": "A password must be provided.",
+    "NEGATIVE_QUANTITY_ERROR": "The quantity for an OrderItem cannot be negative",
+    "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",
+    "ORDER_STATE_TRANSITION_ERROR": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
+    "PASSWORD_ALREADY_SET_ERROR": "A password has already been set during registration",
+    "PASSWORD_RESET_TOKEN_EXPIRED_ERROR": "Password reset token has expired",
+    "PASSWORD_RESET_TOKEN_INVALID_ERROR": "Password reset token not recognized",
+    "PAYMENT_DECLINED_ERROR": "The payment was declined",
+    "PAYMENT_FAILED_ERROR": "The payment failed",
+    "VERIFICATION_TOKEN_EXPIRED_ERROR": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
+    "VERIFICATION_TOKEN_INVALID_ERROR": "Verification token not recognized"
+  },
   "message": {
     "asset-to-be-deleted-is-featured": "The selected {assetCount, plural, one {Asset is} other {Assets are}} featured by {products, plural, =0 {} one {1 Product} other {# Products}} {variants, plural, =0 {} one { 1 ProductVariant} other { # ProductVariants}} {collections, plural, =0 {} one { 1 Collection} other { # Collections}}",
+    "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
+    "cannot-transition-to-shipping-when-order-is-empty": "Cannot transition Order to the \"ArrangingShipping\" state when it is empty",
+    "cannot-transition-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",
+    "cannot-transition-unless-all-cancelled": "Cannot transition Order to the \"Cancelled\" state unless all OrderItems are cancelled",
+    "cannot-transition-unless-all-order-items-delivered": "Cannot transition Order to the \"Delivered\" state unless all OrderItems are delivered",
+    "cannot-transition-unless-some-order-items-delivered": "Cannot transition Order to the \"PartiallyDelivered\" state unless some OrderItems are delivered",
+    "cannot-transition-unless-some-order-items-shipped": "Cannot transition Order to the \"PartiallyShipped\" state unless some OrderItems are shipped",
+    "cannot-transition-unless-all-order-items-shipped": "Cannot transition Order to the \"Shipped\" state unless all OrderItems are shipped",
+    "cannot-transition-without-authorized-payments": "Cannot transition Order to the \"PaymentAuthorized\" state when the total is not covered by authorized Payments",
+    "cannot-transition-without-settled-payments": "Cannot transition Order to the \"PaymentSettled\" state when the total is not covered by settled Payments",
     "country-used-in-addresses": "The selected Country cannot be deleted as it is used in {count, plural, one {1 Address} other {# Addresses}}",
     "facet-force-deleted": "The Facet was deleted and its FacetValues were removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-used": "The selected Facet includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both { , } single {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",

+ 1 - 1
packages/core/src/i18n/messages/es.json

@@ -17,7 +17,7 @@
     "no-valid-channel-specified": "No se ha especificado ningún canal válido (asegúrate de que el encabezado 'vendure-token'  sea especificado en el requisito)",
     "order-contents-may-only-be-modified-in-addingitems-state": "Los contenidos de un pedido solo pueden ser modificados durante el \"AddingItems\" estado ",
     "order-does-not-contain-line-with-id": "Este pedido no contiene una línea de pedido con el id { id }",
-    "order-item-quantity-must-be-positive": "{ quantity } no es una cantidad válida para una linea de pedido",
+    "order-item-quantity-must-be-positive": "no es una cantidad válida para una linea de pedido",
     "order-items-limit-exceeded": "No es posible añadir artículos, Un pedido puede consistir de un máximo de { maxItems } artículos ",
     "payment-may-only-be-added-in-arrangingpayment-state": "Un pago sólo puede ser añadido cuando el pedido se encuentra en el estado de \"ArrangingPayment\"",
     "password-reset-token-has-expired": "Este token de restablecimiento de contraseña ha expirado.",

+ 1 - 1
packages/core/src/i18n/messages/pt_BR.json

@@ -61,7 +61,7 @@
     "no-valid-channel-specified": "Nenhum canal válido foi especificado (verifique se o cabeçalho 'token' foi especificado na solicitação)",
     "order-contents-may-only-be-modified-in-addingitems-state": "O conteúdo do pedido pode ser modificado apenas no estado \"AddingItems\"",
     "order-does-not-contain-line-with-id": "Este pedido não contém um OrderLine com o ID { id }",
-    "order-item-quantity-must-be-positive": "{ quantity } não é uma quantidade válida para um OrderItem",
+    "order-item-quantity-must-be-positive": "não é uma quantidade válida para um OrderItem",
     "order-items-limit-exceeded": "Não é possível adicionar itens. Um pedido pode consistir em no máximo { maxItems } itens",
     "order-lines-must-belong-to-same-order": "Todos os OrderLines devem pertencer a um único pedido",
     "payment-may-only-be-added-in-arrangingpayment-state": "Um pagamento pode ser adicionado apenas quando o pedido estiver no estado \"ArrangingPayment\"",

+ 10 - 10
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -72,45 +72,45 @@ export class OrderStateMachine {
     private async onTransitionStart(fromState: OrderState, toState: OrderState, data: OrderTransitionData) {
         if (toState === 'ArrangingPayment') {
             if (data.order.lines.length === 0) {
-                return `error.cannot-transition-to-payment-when-order-is-empty`;
+                return `message.cannot-transition-to-payment-when-order-is-empty`;
             }
             if (!data.order.customer) {
-                return `error.cannot-transition-to-payment-without-customer`;
+                return `message.cannot-transition-to-payment-without-customer`;
             }
         }
         if (toState === 'PaymentAuthorized' && !orderTotalIsCovered(data.order, 'Authorized')) {
-            return `error.cannot-transition-without-authorized-payments`;
+            return `message.cannot-transition-without-authorized-payments`;
         }
         if (toState === 'PaymentSettled' && !orderTotalIsCovered(data.order, 'Settled')) {
-            return `error.cannot-transition-without-settled-payments`;
+            return `message.cannot-transition-without-settled-payments`;
         }
         if (toState === 'Cancelled' && fromState !== 'AddingItems' && fromState !== 'ArrangingPayment') {
             if (!orderItemsAreAllCancelled(data.order)) {
-                return `error.cannot-transition-unless-all-cancelled`;
+                return `message.cannot-transition-unless-all-cancelled`;
             }
         }
         if (toState === 'PartiallyShipped') {
             const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
             if (!orderItemsArePartiallyShipped(orderWithFulfillments)) {
-                return `error.cannot-transition-unless-some-order-items-shipped`;
+                return `message.cannot-transition-unless-some-order-items-shipped`;
             }
         }
         if (toState === 'Shipped') {
             const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
             if (!orderItemsAreShipped(orderWithFulfillments)) {
-                return `error.cannot-transition-unless-all-order-items-shipped`;
+                return `message.cannot-transition-unless-all-order-items-shipped`;
             }
         }
         if (toState === 'PartiallyDelivered') {
             const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
             if (!orderItemsArePartiallyDelivered(orderWithFulfillments)) {
-                return `error.cannot-transition-unless-some-order-items-delivered`;
+                return `message.cannot-transition-unless-some-order-items-delivered`;
             }
         }
         if (toState === 'Delivered') {
             const orderWithFulfillments = await this.findOrderWithFulfillments(data.ctx, data.order.id);
             if (!orderItemsAreDelivered(orderWithFulfillments)) {
-                return `error.cannot-transition-unless-all-order-items-delivered`;
+                return `message.cannot-transition-unless-all-order-items-delivered`;
             }
         }
     }
@@ -181,7 +181,7 @@ export class OrderStateMachine {
                         );
                     }
                 }
-                throw new IllegalOperationError(message || 'error.cannot-transition-order-from-to', {
+                throw new IllegalOperationError(message || 'message.cannot-transition-order-from-to', {
                     fromState,
                     toState,
                 });

+ 9 - 18
packages/core/src/service/services/asset.service.ts

@@ -5,7 +5,6 @@ import {
     CreateAssetResult,
     DeletionResponse,
     DeletionResult,
-    ErrorResult,
     UpdateAssetInput,
 } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
@@ -16,8 +15,9 @@ import path from 'path';
 import { Stream } from 'stream';
 
 import { RequestContext } from '../../api/common/request-context';
-import { InternalServerError, UserInputError } from '../../common/error/errors';
-import { isGraphQLError, MimeTypeError } from '../../common/error/generated-graphql-admin-errors';
+import { isGraphQlErrorResult } from '../../common/error/error-result';
+import { InternalServerError } from '../../common/error/errors';
+import { MimeTypeError } from '../../common/error/generated-graphql-admin-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { getAssetType, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -152,27 +152,18 @@ export class AssetService {
         return entity;
     }
 
-    async validateInputMimeTypes(inputs: CreateAssetInput[]): Promise<void> {
-        for (const input of inputs) {
-            const { mimetype } = await input.file;
-            if (!this.validateMimeType(mimetype)) {
-                throw new UserInputError('error.mime-type-not-permitted', { mimetype });
-            }
-        }
-    }
-
     /**
      * Create an Asset based on a file uploaded via the GraphQL API.
      */
     async create(ctx: RequestContext, input: CreateAssetInput): Promise<CreateAssetResult> {
         const { createReadStream, filename, mimetype } = await input.file;
         const stream = createReadStream();
-        const asset = await this.createAssetInternal(ctx, stream, filename, mimetype);
-        if (isGraphQLError(asset)) {
-            return asset;
+        const result = await this.createAssetInternal(ctx, stream, filename, mimetype);
+        if (isGraphQlErrorResult(result)) {
+            return result;
         }
-        this.eventBus.publish(new AssetEvent(ctx, asset, 'created'));
-        return asset;
+        this.eventBus.publish(new AssetEvent(ctx, result, 'created'));
+        return result;
     }
 
     async update(ctx: RequestContext, input: UpdateAssetInput): Promise<Asset> {
@@ -253,7 +244,7 @@ export class AssetService {
     ): Promise<CreateAssetResult> {
         const { assetOptions } = this.configService;
         if (!this.validateMimeType(mimetype)) {
-            return new MimeTypeError('error.mime-type-not-permitted', filename, mimetype);
+            return new MimeTypeError(filename, mimetype);
         }
         const { assetPreviewStrategy, assetStorageStrategy } = assetOptions;
         const sourceFileName = await this.getSourceFileName(filename);

+ 7 - 2
packages/core/src/service/services/auth.service.ts

@@ -4,6 +4,7 @@ import { ID } from '@vendure/common/lib/shared-types';
 import { ApiType } from '../../api/common/get-api-type';
 import { RequestContext } from '../../api/common/request-context';
 import { InternalServerError, NotVerifiedError, UnauthorizedError } from '../../common/error/errors';
+import { InvalidCredentialsError } from '../../common/error/generated-graphql-shop-errors';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import {
     NATIVE_AUTH_STRATEGY_NAME,
@@ -95,14 +96,18 @@ export class AuthService {
     /**
      * Verify the provided password against the one we have for the given user.
      */
-    async verifyUserPassword(ctx: RequestContext, userId: ID, password: string): Promise<boolean> {
+    async verifyUserPassword(
+        ctx: RequestContext,
+        userId: ID,
+        password: string,
+    ): Promise<boolean | InvalidCredentialsError> {
         const nativeAuthenticationStrategy = this.getAuthenticationStrategy(
             'shop',
             NATIVE_AUTH_STRATEGY_NAME,
         );
         const passwordMatches = await nativeAuthenticationStrategy.verifyUserPassword(ctx, userId, password);
         if (!passwordMatches) {
-            throw new UnauthorizedError();
+            return new InvalidCredentialsError();
         }
         return true;
     }

+ 78 - 44
packages/core/src/service/services/customer.service.ts

@@ -1,5 +1,9 @@
 import { Injectable } from '@nestjs/common';
-import { RegisterCustomerInput } from '@vendure/common/lib/generated-shop-types';
+import {
+    RegisterCustomerAccountResult,
+    RegisterCustomerInput,
+    VerifyCustomerAccountResult,
+} from '@vendure/common/lib/generated-shop-types';
 import {
     AddNoteToCustomerInput,
     CreateAddressInput,
@@ -14,12 +18,22 @@ import {
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
 import {
     EntityNotFoundError,
     IllegalOperationError,
     InternalServerError,
     UserInputError,
 } from '../../common/error/errors';
+import {
+    EmailAddressConflictError,
+    IdentifierChangeTokenExpiredError,
+    IdentifierChangeTokenInvalidError,
+    MissingPasswordError,
+    PasswordResetTokenExpiredError,
+    PasswordResetTokenInvalidError,
+    VerificationTokenInvalidError,
+} from '../../common/error/generated-graphql-shop-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../config/auth/native-authentication-strategy';
@@ -176,7 +190,12 @@ export class CustomerService {
         if (password && password !== '') {
             const verificationToken = customer.user.getNativeAuthenticationMethod().verificationToken;
             if (verificationToken) {
-                customer.user = await this.userService.verifyUserByToken(ctx, verificationToken);
+                const result = await this.userService.verifyUserByToken(ctx, verificationToken);
+                if (isGraphQlErrorResult(result)) {
+                    // TODO: what to do with an error result here?
+                } else {
+                    customer.user = result;
+                }
             }
         } else {
             this.eventBus.publish(new AccountRegistrationEvent(ctx, customer.user));
@@ -206,10 +225,13 @@ export class CustomerService {
         return createdCustomer;
     }
 
-    async registerCustomerAccount(ctx: RequestContext, input: RegisterCustomerInput): Promise<boolean> {
+    async registerCustomerAccount(
+        ctx: RequestContext,
+        input: RegisterCustomerInput,
+    ): Promise<RegisterCustomerAccountResult | EmailAddressConflictError> {
         if (!this.configService.authOptions.requireVerification) {
             if (!input.password) {
-                throw new UserInputError(`error.missing-password-on-registration`);
+                return new MissingPasswordError();
             }
         }
         let user = await this.userService.getUserByEmailAddress(ctx, input.emailAddress);
@@ -220,7 +242,7 @@ export class CustomerService {
             if (hasNativeAuthMethod) {
                 // If the user has already been verified and has already
                 // registered with the native authentication strategy, do nothing.
-                return false;
+                return { success: true };
             }
         }
         const customFields = (input as any).customFields;
@@ -232,6 +254,9 @@ export class CustomerService {
             phoneNumber: input.phoneNumber || '',
             ...(customFields ? { customFields } : {}),
         });
+        if (isGraphQlErrorResult(customer)) {
+            return customer;
+        }
         await this.historyService.createHistoryEntryForCustomer({
             customerId: customer.id,
             ctx,
@@ -272,7 +297,7 @@ export class CustomerService {
                 },
             });
         }
-        return true;
+        return { success: true };
     }
 
     async refreshVerificationToken(ctx: RequestContext, emailAddress: string): Promise<void> {
@@ -289,23 +314,24 @@ export class CustomerService {
         ctx: RequestContext,
         verificationToken: string,
         password?: string,
-    ): Promise<Customer | undefined> {
-        const user = await this.userService.verifyUserByToken(ctx, verificationToken, password);
-        if (user) {
-            const customer = await this.findOneByUserId(ctx, user.id);
-            if (!customer) {
-                throw new InternalServerError('error.cannot-locate-customer-for-user');
-            }
-            await this.historyService.createHistoryEntryForCustomer({
-                customerId: customer.id,
-                ctx,
-                type: HistoryEntryType.CUSTOMER_VERIFIED,
-                data: {
-                    strategy: NATIVE_AUTH_STRATEGY_NAME,
-                },
-            });
-            return this.findOneByUserId(ctx, user.id);
+    ): Promise<ErrorResultUnion<VerifyCustomerAccountResult, Customer>> {
+        const result = await this.userService.verifyUserByToken(ctx, verificationToken, password);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        const customer = await this.findOneByUserId(ctx, result.id);
+        if (!customer) {
+            throw new InternalServerError('error.cannot-locate-customer-for-user');
         }
+        await this.historyService.createHistoryEntryForCustomer({
+            customerId: customer.id,
+            ctx,
+            type: HistoryEntryType.CUSTOMER_VERIFIED,
+            data: {
+                strategy: NATIVE_AUTH_STRATEGY_NAME,
+            },
+        });
+        return assertFound(this.findOneByUserId(ctx, result.id));
     }
 
     async requestPasswordReset(ctx: RequestContext, emailAddress: string): Promise<void> {
@@ -329,34 +355,35 @@ export class CustomerService {
         ctx: RequestContext,
         passwordResetToken: string,
         password: string,
-    ): Promise<Customer | undefined> {
-        const user = await this.userService.resetPasswordByToken(ctx, passwordResetToken, password);
-        if (user) {
-            const customer = await this.findOneByUserId(ctx, user.id);
-            if (!customer) {
-                throw new InternalServerError('error.cannot-locate-customer-for-user');
-            }
-            await this.historyService.createHistoryEntryForCustomer({
-                customerId: customer.id,
-                ctx,
-                type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED,
-                data: {},
-            });
-            return customer;
+    ): Promise<User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError> {
+        const result = await this.userService.resetPasswordByToken(ctx, passwordResetToken, password);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        const customer = await this.findOneByUserId(ctx, result.id);
+        if (!customer) {
+            throw new InternalServerError('error.cannot-locate-customer-for-user');
         }
+        await this.historyService.createHistoryEntryForCustomer({
+            customerId: customer.id,
+            ctx,
+            type: HistoryEntryType.CUSTOMER_PASSWORD_RESET_VERIFIED,
+            data: {},
+        });
+        return result;
     }
 
     async requestUpdateEmailAddress(
         ctx: RequestContext,
         userId: ID,
         newEmailAddress: string,
-    ): Promise<boolean> {
+    ): Promise<boolean | EmailAddressConflictError> {
         const userWithConflictingIdentifier = await this.userService.getUserByEmailAddress(
             ctx,
             newEmailAddress,
         );
         if (userWithConflictingIdentifier) {
-            throw new UserInputError('error.email-address-not-available');
+            return new EmailAddressConflictError();
         }
         const user = await this.userService.getUserById(ctx, userId);
         if (!user) {
@@ -401,8 +428,15 @@ export class CustomerService {
         }
     }
 
-    async updateEmailAddress(ctx: RequestContext, token: string): Promise<boolean> {
-        const { user, oldIdentifier } = await this.userService.changeIdentifierByToken(ctx, token);
+    async updateEmailAddress(
+        ctx: RequestContext,
+        token: string,
+    ): Promise<boolean | IdentifierChangeTokenInvalidError | IdentifierChangeTokenExpiredError> {
+        const result = await this.userService.changeIdentifierByToken(ctx, token);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        const { user, oldIdentifier } = result;
         if (!user) {
             return false;
         }
@@ -448,8 +482,8 @@ export class CustomerService {
     async createOrUpdate(
         ctx: RequestContext,
         input: Partial<CreateCustomerInput> & { emailAddress: string },
-        throwOnExistingUser: boolean = false,
-    ): Promise<Customer> {
+        errorOnExistingUser: boolean = false,
+    ): Promise<Customer | EmailAddressConflictError> {
         input.emailAddress = normalizeEmailAddress(input.emailAddress);
         let customer: Customer;
         const existing = await this.connection.getRepository(ctx, Customer).findOne({
@@ -460,9 +494,9 @@ export class CustomerService {
             },
         });
         if (existing) {
-            if (existing.user && throwOnExistingUser) {
+            if (existing.user && errorOnExistingUser) {
                 // It is not permitted to modify an existing *registered* Customer
-                throw new IllegalOperationError('error.cannot-use-registered-email-address-for-guest-order');
+                return new EmailAddressConflictError();
             }
             customer = patchEntity(existing, input);
             customer.channels.push(await this.connection.getEntityOrThrow(ctx, Channel, ctx.channelId));

+ 107 - 36
packages/core/src/service/services/order.service.ts

@@ -1,5 +1,12 @@
 import { Injectable } from '@nestjs/common';
-import { PaymentInput } from '@vendure/common/lib/generated-shop-types';
+import {
+    AddPaymentToOrderResult,
+    ApplyCouponCodeResult,
+    PaymentInput,
+    RemoveOrderItemsResult,
+    SetOrderShippingMethodResult,
+    UpdateOrderItemsResult,
+} from '@vendure/common/lib/generated-shop-types';
 import {
     AddNoteToOrderInput,
     CancelOrderInput,
@@ -20,13 +27,22 @@ import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../api/common/request-context';
+import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
 import {
     EntityNotFoundError,
     IllegalOperationError,
     InternalServerError,
-    OrderItemsLimitError,
     UserInputError,
 } from '../../common/error/errors';
+import {
+    NegativeQuantityError,
+    OrderLimitError,
+    OrderModificationError,
+    OrderPaymentStateError,
+    OrderStateTransitionError,
+    PaymentDeclinedError,
+    PaymentFailedError,
+} from '../../common/error/generated-graphql-shop-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -276,11 +292,15 @@ export class OrderService {
         productVariantId: ID,
         quantity: number,
         customFields?: { [key: string]: any },
-    ): Promise<Order> {
-        this.assertQuantityIsPositive(quantity);
+    ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        this.assertAddingItemsState(order);
-        this.assertNotOverOrderItemsLimit(order, quantity);
+        const validationError =
+            this.assertQuantityIsPositive(quantity) ||
+            this.assertAddingItemsState(order) ||
+            this.assertNotOverOrderItemsLimit(order, quantity);
+        if (validationError) {
+            return validationError;
+        }
         const productVariant = await this.getProductVariantOrThrow(ctx, productVariantId);
         let orderLine = order.lines.find(line => {
             return (
@@ -304,7 +324,7 @@ export class OrderService {
         orderLineId: ID,
         quantity?: number | null,
         customFields?: { [key: string]: any },
-    ): Promise<Order> {
+    ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const { priceCalculationStrategy } = this.configService.orderOptions;
         const order =
             orderIdOrOrder instanceof Order
@@ -314,11 +334,15 @@ export class OrderService {
         if (customFields != null) {
             orderLine.customFields = customFields;
         }
-        this.assertAddingItemsState(order);
         if (quantity != null) {
-            this.assertQuantityIsPositive(quantity);
             const currentQuantity = orderLine.quantity;
-            this.assertNotOverOrderItemsLimit(order, quantity - currentQuantity);
+            const validationError =
+                this.assertAddingItemsState(order) ||
+                this.assertQuantityIsPositive(quantity) ||
+                this.assertNotOverOrderItemsLimit(order, quantity - currentQuantity);
+            if (validationError) {
+                return validationError;
+            }
             if (currentQuantity < quantity) {
                 if (!orderLine.items) {
                     orderLine.items = [];
@@ -349,9 +373,16 @@ export class OrderService {
         return this.applyPriceAdjustments(ctx, order, orderLine);
     }
 
-    async removeItemFromOrder(ctx: RequestContext, orderId: ID, orderLineId: ID): Promise<Order> {
+    async removeItemFromOrder(
+        ctx: RequestContext,
+        orderId: ID,
+        orderLineId: ID,
+    ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        this.assertAddingItemsState(order);
+        const validationError = this.assertAddingItemsState(order);
+        if (validationError) {
+            return validationError;
+        }
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
         order.lines = order.lines.filter(line => !idsAreEqual(line.id, orderLineId));
         const updatedOrder = await this.applyPriceAdjustments(ctx, order);
@@ -359,31 +390,44 @@ export class OrderService {
         return updatedOrder;
     }
 
-    async removeAllItemsFromOrder(ctx: RequestContext, orderId: ID): Promise<Order> {
+    async removeAllItemsFromOrder(
+        ctx: RequestContext,
+        orderId: ID,
+    ): Promise<ErrorResultUnion<RemoveOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        this.assertAddingItemsState(order);
+        const validationError = this.assertAddingItemsState(order);
+        if (validationError) {
+            return validationError;
+        }
         await this.connection.getRepository(ctx, OrderLine).remove(order.lines);
         order.lines = [];
         const updatedOrder = await this.applyPriceAdjustments(ctx, order);
         return updatedOrder;
     }
 
-    async applyCouponCode(ctx: RequestContext, orderId: ID, couponCode: string) {
+    async applyCouponCode(
+        ctx: RequestContext,
+        orderId: ID,
+        couponCode: string,
+    ): Promise<ErrorResultUnion<ApplyCouponCodeResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.couponCodes.includes(couponCode)) {
             return order;
         }
-        const promotion = await this.promotionService.validateCouponCode(
+        const validationResult = await this.promotionService.validateCouponCode(
             ctx,
             couponCode,
             order.customer && order.customer.id,
         );
+        if (isGraphQlErrorResult(validationResult)) {
+            return validationResult;
+        }
         order.couponCodes.push(couponCode);
         await this.historyService.createHistoryEntryForOrder({
             ctx,
             orderId: order.id,
             type: HistoryEntryType.ORDER_COUPON_APPLIED,
-            data: { couponCode, promotionId: promotion.id },
+            data: { couponCode, promotionId: validationResult.id },
         });
         return this.applyPriceAdjustments(ctx, order);
     }
@@ -442,9 +486,16 @@ export class OrderService {
         }));
     }
 
-    async setShippingMethod(ctx: RequestContext, orderId: ID, shippingMethodId: ID): Promise<Order> {
+    async setShippingMethod(
+        ctx: RequestContext,
+        orderId: ID,
+        shippingMethodId: ID,
+    ): Promise<ErrorResultUnion<SetOrderShippingMethodResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
-        this.assertAddingItemsState(order);
+        const validationError = this.assertAddingItemsState(order);
+        if (validationError) {
+            return validationError;
+        }
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const selectedMethod = eligibleMethods.find(m => idsAreEqual(m.method.id, shippingMethodId));
         if (!selectedMethod) {
@@ -456,11 +507,20 @@ export class OrderService {
         return this.connection.getRepository(ctx, Order).save(order);
     }
 
-    async transitionToState(ctx: RequestContext, orderId: ID, state: OrderState): Promise<Order> {
+    async transitionToState(
+        ctx: RequestContext,
+        orderId: ID,
+        state: OrderState,
+    ): Promise<Order | OrderStateTransitionError> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         order.payments = await this.getOrderPayments(ctx, orderId);
         const fromState = order.state;
-        await this.orderStateMachine.transition(ctx, order, state);
+        try {
+            await this.orderStateMachine.transition(ctx, order, state);
+        } catch (e) {
+            const transitionError = ctx.translate(e.message, { fromState, toState: state });
+            return new OrderStateTransitionError(transitionError, fromState, state);
+        }
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, state, ctx, order));
         return order;
@@ -511,10 +571,14 @@ export class OrderService {
         }
     }
 
-    async addPaymentToOrder(ctx: RequestContext, orderId: ID, input: PaymentInput): Promise<Order> {
+    async addPaymentToOrder(
+        ctx: RequestContext,
+        orderId: ID,
+        input: PaymentInput,
+    ): Promise<ErrorResultUnion<AddPaymentToOrderResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.state !== 'ArrangingPayment') {
-            throw new IllegalOperationError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
+            return new OrderPaymentStateError();
         }
         const payment = await this.paymentMethodService.createPayment(
             ctx,
@@ -528,10 +592,10 @@ export class OrderService {
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
 
         if (payment.state === 'Error') {
-            // TODO: we need to return an Error response as per
-            // https://github.com/vendure-ecommerce/vendure/issues/437
-            // Throwing an error rolls back all changes, which we do not want.
-            // throw new InternalServerError(payment.errorMessage);
+            return new PaymentFailedError(payment.errorMessage);
+        }
+        if (payment.state === 'Declined') {
+            return new PaymentDeclinedError(payment.errorMessage);
         }
 
         if (orderTotalIsCovered(order, 'Settled')) {
@@ -768,9 +832,12 @@ export class OrderService {
         if (order.couponCodes) {
             let codesRemoved = false;
             for (const couponCode of order.couponCodes.slice()) {
-                try {
-                    await this.promotionService.validateCouponCode(ctx, couponCode, customer.id);
-                } catch (err) {
+                const validationResult = await this.promotionService.validateCouponCode(
+                    ctx,
+                    couponCode,
+                    customer.id,
+                );
+                if (isGraphQlErrorResult(validationResult)) {
                     order.couponCodes = order.couponCodes.filter(c => c !== couponCode);
                     codesRemoved = true;
                 }
@@ -844,8 +911,12 @@ export class OrderService {
             await this.connection.getRepository(ctx, Order).delete(orderToDelete.id);
         }
         if (order && linesToInsert) {
+            const orderId = order.id;
             for (const line of linesToInsert) {
-                order = await this.addItemToOrder(ctx, order.id, line.productVariantId, line.quantity);
+                const result = await this.addItemToOrder(ctx, orderId, line.productVariantId, line.quantity);
+                if (!isGraphQlErrorResult(result)) {
+                    order = result;
+                }
             }
         }
         const customer = await this.customerService.findOneByUserId(ctx, user.id);
@@ -896,20 +967,20 @@ export class OrderService {
     }
 
     /**
-     * Throws if quantity is negative.
+     * Returns error if quantity is negative.
      */
     private assertQuantityIsPositive(quantity: number) {
         if (quantity < 0) {
-            throw new IllegalOperationError(`error.order-item-quantity-must-be-positive`, { quantity });
+            return new NegativeQuantityError();
         }
     }
 
     /**
-     * Throws if the Order is not in the "AddingItems" state.
+     * Returns error if the Order is not in the "AddingItems" state.
      */
     private assertAddingItemsState(order: Order) {
         if (order.state !== 'AddingItems') {
-            throw new IllegalOperationError(`error.order-contents-may-only-be-modified-in-addingitems-state`);
+            return new OrderModificationError();
         }
     }
 
@@ -921,7 +992,7 @@ export class OrderService {
         const currentItemsCount = order.lines.reduce((count, line) => count + line.quantity, 0);
         const { orderItemsLimit } = this.configService.orderOptions;
         if (orderItemsLimit < currentItemsCount + quantityToAdd) {
-            throw new OrderItemsLimitError(orderItemsLimit);
+            return new OrderLimitError(orderItemsLimit);
         }
     }
 

+ 12 - 6
packages/core/src/service/services/promotion.service.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
+import { ApplyCouponCodeResult } from '@vendure/common/lib/generated-shop-types';
 import {
     Adjustment,
     AdjustmentType,
@@ -15,12 +16,13 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 
 import { RequestContext } from '../../api/common/request-context';
+import { JustErrorResults } from '../../common/error/error-result';
+import { UserInputError } from '../../common/error/errors';
 import {
     CouponCodeExpiredError,
     CouponCodeInvalidError,
     CouponCodeLimitError,
-    UserInputError,
-} from '../../common/error/errors';
+} from '../../common/error/generated-graphql-shop-errors';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
@@ -142,7 +144,11 @@ export class PromotionService {
         };
     }
 
-    async validateCouponCode(ctx: RequestContext, couponCode: string, customerId?: ID): Promise<Promotion> {
+    async validateCouponCode(
+        ctx: RequestContext,
+        couponCode: string,
+        customerId?: ID,
+    ): Promise<JustErrorResults<ApplyCouponCodeResult> | Promotion> {
         const promotion = await this.connection.getRepository(ctx, Promotion).findOne({
             where: {
                 couponCode,
@@ -151,15 +157,15 @@ export class PromotionService {
             },
         });
         if (!promotion) {
-            throw new CouponCodeInvalidError(couponCode);
+            return new CouponCodeInvalidError(couponCode);
         }
         if (promotion.endsAt && +promotion.endsAt < +new Date()) {
-            throw new CouponCodeExpiredError(couponCode);
+            return new CouponCodeExpiredError(couponCode);
         }
         if (customerId && promotion.perCustomerUsageLimit != null) {
             const usageCount = await this.countPromotionUsagesForCustomer(ctx, promotion.id, customerId);
             if (promotion.perCustomerUsageLimit <= usageCount) {
-                throw new CouponCodeLimitError(promotion.perCustomerUsageLimit);
+                return new CouponCodeLimitError(couponCode, promotion.perCustomerUsageLimit);
             }
         }
         return promotion;

+ 38 - 26
packages/core/src/service/services/user.service.ts

@@ -1,16 +1,21 @@
 import { Injectable } from '@nestjs/common';
+import { VerifyCustomerAccountResult } from '@vendure/common/lib/generated-shop-types';
 import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { ErrorResultUnion } from '../../common/error/error-result';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import {
-    IdentifierChangeTokenError,
     IdentifierChangeTokenExpiredError,
-    InternalServerError,
+    IdentifierChangeTokenInvalidError,
+    InvalidCredentialsError,
+    MissingPasswordError,
+    PasswordAlreadySetError,
     PasswordResetTokenExpiredError,
-    UnauthorizedError,
-    UserInputError,
+    PasswordResetTokenInvalidError,
     VerificationTokenExpiredError,
-} from '../../common/error/errors';
+    VerificationTokenInvalidError,
+} from '../../common/error/generated-graphql-shop-errors';
 import { ConfigService } from '../../config/config.service';
 import { NativeAuthenticationMethod } from '../../entity/authentication-method/native-authentication-method.entity';
 import { User } from '../../entity/user/user.entity';
@@ -114,7 +119,7 @@ export class UserService {
         ctx: RequestContext,
         verificationToken: string,
         password?: string,
-    ): Promise<User | undefined> {
+    ): Promise<ErrorResultUnion<VerifyCustomerAccountResult, User>> {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
@@ -127,11 +132,11 @@ export class UserService {
                 const nativeAuthMethod = user.getNativeAuthenticationMethod();
                 if (!password) {
                     if (!nativeAuthMethod.passwordHash) {
-                        throw new UserInputError(`error.password-required-for-verification`);
+                        return new MissingPasswordError();
                     }
                 } else {
                     if (!!nativeAuthMethod.passwordHash) {
-                        throw new UserInputError(`error.password-already-set-during-registration`);
+                        return new PasswordAlreadySetError();
                     }
                     nativeAuthMethod.passwordHash = await this.passwordCipher.hash(password);
                 }
@@ -140,8 +145,10 @@ export class UserService {
                 await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod);
                 return this.connection.getRepository(ctx, User).save(user);
             } else {
-                throw new VerificationTokenExpiredError();
+                return new VerificationTokenExpiredError();
             }
+        } else {
+            return new VerificationTokenInvalidError();
         }
     }
 
@@ -160,30 +167,35 @@ export class UserService {
         ctx: RequestContext,
         passwordResetToken: string,
         password: string,
-    ): Promise<User | undefined> {
+    ): Promise<User | PasswordResetTokenExpiredError | PasswordResetTokenInvalidError> {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
             .leftJoinAndSelect('user.authenticationMethods', 'authenticationMethod')
             .where('authenticationMethod.passwordResetToken = :passwordResetToken', { passwordResetToken })
             .getOne();
-        if (user) {
-            if (this.verificationTokenGenerator.verifyVerificationToken(passwordResetToken)) {
-                const nativeAuthMethod = user.getNativeAuthenticationMethod();
-                nativeAuthMethod.passwordHash = await this.passwordCipher.hash(password);
-                nativeAuthMethod.passwordResetToken = null;
-                await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod);
-                return this.connection.getRepository(ctx, User).save(user);
-            } else {
-                throw new PasswordResetTokenExpiredError();
-            }
+        if (!user) {
+            return new PasswordResetTokenInvalidError();
+        }
+        if (this.verificationTokenGenerator.verifyVerificationToken(passwordResetToken)) {
+            const nativeAuthMethod = user.getNativeAuthenticationMethod();
+            nativeAuthMethod.passwordHash = await this.passwordCipher.hash(password);
+            nativeAuthMethod.passwordResetToken = null;
+            await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod);
+            return this.connection.getRepository(ctx, User).save(user);
+        } else {
+            return new PasswordResetTokenExpiredError();
         }
     }
 
     async changeIdentifierByToken(
         ctx: RequestContext,
         token: string,
-    ): Promise<{ user: User; oldIdentifier: string }> {
+    ): Promise<
+        | { user: User; oldIdentifier: string }
+        | IdentifierChangeTokenInvalidError
+        | IdentifierChangeTokenExpiredError
+    > {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
@@ -193,10 +205,10 @@ export class UserService {
             })
             .getOne();
         if (!user) {
-            throw new IdentifierChangeTokenError();
+            return new IdentifierChangeTokenInvalidError();
         }
         if (!this.verificationTokenGenerator.verifyVerificationToken(token)) {
-            throw new IdentifierChangeTokenExpiredError();
+            return new IdentifierChangeTokenExpiredError();
         }
         const nativeAuthMethod = user.getNativeAuthenticationMethod();
         const pendingIdentifier = nativeAuthMethod.pendingIdentifier;
@@ -220,7 +232,7 @@ export class UserService {
         userId: ID,
         currentPassword: string,
         newPassword: string,
-    ): Promise<boolean> {
+    ): Promise<boolean | InvalidCredentialsError> {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
@@ -229,12 +241,12 @@ export class UserService {
             .where('user.id = :id', { id: userId })
             .getOne();
         if (!user) {
-            throw new InternalServerError(`error.no-active-user-id`);
+            throw new EntityNotFoundError('User', userId);
         }
         const nativeAuthMethod = user.getNativeAuthenticationMethod();
         const matches = await this.passwordCipher.check(currentPassword, nativeAuthMethod.passwordHash);
         if (!matches) {
-            throw new UnauthorizedError();
+            return new InvalidCredentialsError();
         }
         nativeAuthMethod.passwordHash = await this.passwordCipher.hash(newPassword);
         await this.connection

+ 1 - 1
packages/dev-server/dev-config.ts

@@ -120,7 +120,7 @@ function getDbConfig(): ConnectionOptions {
         default:
             console.log('Using mysql connection');
             return {
-                synchronize: true,
+                synchronize: false,
                 type: 'mysql',
                 host: '127.0.0.1',
                 port: 3306,

+ 61 - 169
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -29,7 +29,6 @@ export type AddNoteToOrderInput = {
 };
 
 export type Address = Node & {
-    __typename?: 'Address';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -48,7 +47,6 @@ export type Address = Node & {
 };
 
 export type Adjustment = {
-    __typename?: 'Adjustment';
     adjustmentSource: Scalars['String'];
     type: AdjustmentType;
     description: Scalars['String'];
@@ -66,7 +64,6 @@ export enum AdjustmentType {
 }
 
 export type Administrator = Node & {
-    __typename?: 'Administrator';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -85,7 +82,6 @@ export type AdministratorFilterParameter = {
 };
 
 export type AdministratorList = PaginatedList & {
-    __typename?: 'AdministratorList';
     items: Array<Administrator>;
     totalItems: Scalars['Int'];
 };
@@ -107,7 +103,6 @@ export type AdministratorSortParameter = {
 };
 
 export type Asset = Node & {
-    __typename?: 'Asset';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -136,7 +131,6 @@ export type AssetFilterParameter = {
 };
 
 export type AssetList = PaginatedList & {
-    __typename?: 'AssetList';
     items: Array<Asset>;
     totalItems: Scalars['Int'];
 };
@@ -178,7 +172,6 @@ export type AuthenticationInput = {
 };
 
 export type AuthenticationMethod = Node & {
-    __typename?: 'AuthenticationMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -186,7 +179,6 @@ export type AuthenticationMethod = Node & {
 };
 
 export type BooleanCustomFieldConfig = CustomField & {
-    __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -202,7 +194,6 @@ export type BooleanOperators = {
 
 export type Cancellation = Node &
     StockMovement & {
-        __typename?: 'Cancellation';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -221,7 +212,6 @@ export type CancelOrderInput = {
 };
 
 export type Channel = Node & {
-    __typename?: 'Channel';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -235,7 +225,6 @@ export type Channel = Node & {
 };
 
 export type Collection = Node & {
-    __typename?: 'Collection';
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -261,7 +250,6 @@ export type CollectionProductVariantsArgs = {
 };
 
 export type CollectionBreadcrumb = {
-    __typename?: 'CollectionBreadcrumb';
     id: Scalars['ID'];
     name: Scalars['String'];
     slug: Scalars['String'];
@@ -279,7 +267,6 @@ export type CollectionFilterParameter = {
 };
 
 export type CollectionList = PaginatedList & {
-    __typename?: 'CollectionList';
     items: Array<Collection>;
     totalItems: Scalars['Int'];
 };
@@ -302,7 +289,6 @@ export type CollectionSortParameter = {
 };
 
 export type CollectionTranslation = {
-    __typename?: 'CollectionTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -313,13 +299,11 @@ export type CollectionTranslation = {
 };
 
 export type ConfigArg = {
-    __typename?: 'ConfigArg';
     name: Scalars['String'];
     value: Scalars['String'];
 };
 
 export type ConfigArgDefinition = {
-    __typename?: 'ConfigArgDefinition';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -334,13 +318,11 @@ export type ConfigArgInput = {
 };
 
 export type ConfigurableOperation = {
-    __typename?: 'ConfigurableOperation';
     code: Scalars['String'];
     args: Array<ConfigArg>;
 };
 
 export type ConfigurableOperationDefinition = {
-    __typename?: 'ConfigurableOperationDefinition';
     code: Scalars['String'];
     args: Array<ConfigArgDefinition>;
     description: Scalars['String'];
@@ -352,7 +334,6 @@ export type ConfigurableOperationInput = {
 };
 
 export type Coordinate = {
-    __typename?: 'Coordinate';
     x: Scalars['Float'];
     y: Scalars['Float'];
 };
@@ -363,7 +344,6 @@ export type CoordinateInput = {
 };
 
 export type Country = Node & {
-    __typename?: 'Country';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -384,7 +364,6 @@ export type CountryFilterParameter = {
 };
 
 export type CountryList = PaginatedList & {
-    __typename?: 'CountryList';
     items: Array<Country>;
     totalItems: Scalars['Int'];
 };
@@ -405,7 +384,6 @@ export type CountrySortParameter = {
 };
 
 export type CountryTranslation = {
-    __typename?: 'CountryTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -932,14 +910,12 @@ export enum CurrencyCode {
 }
 
 export type CurrentUser = {
-    __typename?: 'CurrentUser';
     id: Scalars['ID'];
     identifier: Scalars['String'];
     channels: Array<CurrentUserChannel>;
 };
 
 export type CurrentUserChannel = {
-    __typename?: 'CurrentUserChannel';
     id: Scalars['ID'];
     token: Scalars['String'];
     code: Scalars['String'];
@@ -947,7 +923,6 @@ export type CurrentUserChannel = {
 };
 
 export type Customer = Node & {
-    __typename?: 'Customer';
     groups: Array<CustomerGroup>;
     history: HistoryEntryList;
     id: Scalars['ID'];
@@ -983,7 +958,6 @@ export type CustomerFilterParameter = {
 };
 
 export type CustomerGroup = Node & {
-    __typename?: 'CustomerGroup';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1002,7 +976,6 @@ export type CustomerGroupFilterParameter = {
 };
 
 export type CustomerGroupList = PaginatedList & {
-    __typename?: 'CustomerGroupList';
     items: Array<CustomerGroup>;
     totalItems: Scalars['Int'];
 };
@@ -1022,7 +995,6 @@ export type CustomerGroupSortParameter = {
 };
 
 export type CustomerList = PaginatedList & {
-    __typename?: 'CustomerList';
     items: Array<Customer>;
     totalItems: Scalars['Int'];
 };
@@ -1064,7 +1036,6 @@ export type CustomFieldConfig =
     | DateTimeCustomFieldConfig;
 
 export type CustomFields = {
-    __typename?: 'CustomFields';
     Address: Array<CustomFieldConfig>;
     Collection: Array<CustomFieldConfig>;
     Customer: Array<CustomFieldConfig>;
@@ -1098,7 +1069,6 @@ export type DateRange = {
  * See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#Additional_attributes
  */
 export type DateTimeCustomFieldConfig = CustomField & {
-    __typename?: 'DateTimeCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1112,7 +1082,6 @@ export type DateTimeCustomFieldConfig = CustomField & {
 };
 
 export type DeletionResponse = {
-    __typename?: 'DeletionResponse';
     result: DeletionResult;
     message?: Maybe<Scalars['String']>;
 };
@@ -1125,8 +1094,9 @@ export enum DeletionResult {
 }
 
 export enum ErrorCode {
-    UnknownError = 'UnknownError',
-    MimeTypeError = 'MimeTypeError',
+    UNKNOWN_ERROR = 'UNKNOWN_ERROR',
+    MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
+    ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
 }
 
 export type ErrorResult = {
@@ -1135,7 +1105,6 @@ export type ErrorResult = {
 };
 
 export type Facet = Node & {
-    __typename?: 'Facet';
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -1158,7 +1127,6 @@ export type FacetFilterParameter = {
 };
 
 export type FacetList = PaginatedList & {
-    __typename?: 'FacetList';
     items: Array<Facet>;
     totalItems: Scalars['Int'];
 };
@@ -1179,7 +1147,6 @@ export type FacetSortParameter = {
 };
 
 export type FacetTranslation = {
-    __typename?: 'FacetTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1195,7 +1162,6 @@ export type FacetTranslationInput = {
 };
 
 export type FacetValue = Node & {
-    __typename?: 'FacetValue';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1212,13 +1178,11 @@ export type FacetValue = Node & {
  * by the search, and in what quantity.
  */
 export type FacetValueResult = {
-    __typename?: 'FacetValueResult';
     facetValue: FacetValue;
     count: Scalars['Int'];
 };
 
 export type FacetValueTranslation = {
-    __typename?: 'FacetValueTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1234,7 +1198,6 @@ export type FacetValueTranslationInput = {
 };
 
 export type FloatCustomFieldConfig = CustomField & {
-    __typename?: 'FloatCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1248,7 +1211,6 @@ export type FloatCustomFieldConfig = CustomField & {
 };
 
 export type Fulfillment = Node & {
-    __typename?: 'Fulfillment';
     nextStates: Array<Scalars['String']>;
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -1266,7 +1228,6 @@ export type FulfillOrderInput = {
 };
 
 export type GlobalSettings = {
-    __typename?: 'GlobalSettings';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1277,7 +1238,6 @@ export type GlobalSettings = {
 };
 
 export type HistoryEntry = Node & {
-    __typename?: 'HistoryEntry';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -1295,7 +1255,6 @@ export type HistoryEntryFilterParameter = {
 };
 
 export type HistoryEntryList = PaginatedList & {
-    __typename?: 'HistoryEntryList';
     items: Array<HistoryEntry>;
     totalItems: Scalars['Int'];
 };
@@ -1340,14 +1299,12 @@ export enum HistoryEntryType {
 }
 
 export type ImportInfo = {
-    __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
     processed: Scalars['Int'];
     imported: Scalars['Int'];
 };
 
 export type IntCustomFieldConfig = CustomField & {
-    __typename?: 'IntCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1361,7 +1318,6 @@ export type IntCustomFieldConfig = CustomField & {
 };
 
 export type Job = Node & {
-    __typename?: 'Job';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     startedAt?: Maybe<Scalars['DateTime']>;
@@ -1388,7 +1344,6 @@ export type JobFilterParameter = {
 };
 
 export type JobList = PaginatedList & {
-    __typename?: 'JobList';
     items: Array<Job>;
     totalItems: Scalars['Int'];
 };
@@ -1401,7 +1356,6 @@ export type JobListOptions = {
 };
 
 export type JobQueue = {
-    __typename?: 'JobQueue';
     name: Scalars['String'];
     running: Scalars['Boolean'];
 };
@@ -1757,7 +1711,6 @@ export enum LanguageCode {
 }
 
 export type LocaleStringCustomFieldConfig = CustomField & {
-    __typename?: 'LocaleStringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -1770,7 +1723,6 @@ export type LocaleStringCustomFieldConfig = CustomField & {
 };
 
 export type LocalizedString = {
-    __typename?: 'LocalizedString';
     languageCode: LanguageCode;
     value: Scalars['String'];
 };
@@ -1781,12 +1733,10 @@ export enum LogicalOperator {
 }
 
 export type LoginResult = {
-    __typename?: 'LoginResult';
     user: CurrentUser;
 };
 
 export type MimeTypeError = ErrorResult & {
-    __typename?: 'MimeTypeError';
     code: ErrorCode;
     message: Scalars['String'];
     fileName: Scalars['String'];
@@ -1800,7 +1750,6 @@ export type MoveCollectionInput = {
 };
 
 export type Mutation = {
-    __typename?: 'Mutation';
     /** Create a new Administrator */
     createAdministrator: Administrator;
     /** Update an existing Administrator */
@@ -1866,7 +1815,7 @@ export type Mutation = {
     /** Update an existing Address */
     updateCustomerAddress: Address;
     /** Update an existing Address */
-    deleteCustomerAddress: Scalars['Boolean'];
+    deleteCustomerAddress: Success;
     addNoteToCustomer: Customer;
     updateCustomerNote: HistoryEntry;
     deleteCustomerNote: DeletionResponse;
@@ -1894,7 +1843,7 @@ export type Mutation = {
     addNoteToOrder: Order;
     updateOrderNote: HistoryEntry;
     deleteOrderNote: DeletionResponse;
-    transitionOrderToState?: Maybe<Order>;
+    transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionFulfillmentToState: Fulfillment;
     setOrderCustomFields?: Maybe<Order>;
     /** Update an existing PaymentMethod */
@@ -2366,7 +2315,6 @@ export type NumberRange = {
 };
 
 export type Order = Node & {
-    __typename?: 'Order';
     nextStates: Array<Scalars['String']>;
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
@@ -2405,7 +2353,6 @@ export type OrderHistoryArgs = {
 };
 
 export type OrderAddress = {
-    __typename?: 'OrderAddress';
     fullName?: Maybe<Scalars['String']>;
     company?: Maybe<Scalars['String']>;
     streetLine1?: Maybe<Scalars['String']>;
@@ -2434,7 +2381,6 @@ export type OrderFilterParameter = {
 };
 
 export type OrderItem = Node & {
-    __typename?: 'OrderItem';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2449,7 +2395,6 @@ export type OrderItem = Node & {
 };
 
 export type OrderLine = Node & {
-    __typename?: 'OrderLine';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2471,7 +2416,6 @@ export type OrderLineInput = {
 };
 
 export type OrderList = PaginatedList & {
-    __typename?: 'OrderList';
     items: Array<Order>;
     totalItems: Scalars['Int'];
 };
@@ -2484,7 +2428,6 @@ export type OrderListOptions = {
 };
 
 export type OrderProcessState = {
-    __typename?: 'OrderProcessState';
     name: Scalars['String'];
     to: Array<Scalars['String']>;
 };
@@ -2503,13 +2446,21 @@ export type OrderSortParameter = {
     total?: Maybe<SortOrder>;
 };
 
+/** Returned if there is an error in transitioning the Order state */
+export type OrderStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 export type PaginatedList = {
     items: Array<Node>;
     totalItems: Scalars['Int'];
 };
 
 export type Payment = Node & {
-    __typename?: 'Payment';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2523,7 +2474,6 @@ export type Payment = Node & {
 };
 
 export type PaymentMethod = Node & {
-    __typename?: 'PaymentMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2541,7 +2491,6 @@ export type PaymentMethodFilterParameter = {
 };
 
 export type PaymentMethodList = PaginatedList & {
-    __typename?: 'PaymentMethodList';
     items: Array<PaymentMethod>;
     totalItems: Scalars['Int'];
 };
@@ -2605,13 +2554,11 @@ export enum Permission {
 
 /** The price range where the result has more than one price */
 export type PriceRange = {
-    __typename?: 'PriceRange';
     min: Scalars['Int'];
     max: Scalars['Int'];
 };
 
 export type Product = Node & {
-    __typename?: 'Product';
     enabled: Scalars['Boolean'];
     channels: Array<Channel>;
     id: Scalars['ID'];
@@ -2642,7 +2589,6 @@ export type ProductFilterParameter = {
 };
 
 export type ProductList = PaginatedList & {
-    __typename?: 'ProductList';
     items: Array<Product>;
     totalItems: Scalars['Int'];
 };
@@ -2655,7 +2601,6 @@ export type ProductListOptions = {
 };
 
 export type ProductOption = Node & {
-    __typename?: 'ProductOption';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2669,7 +2614,6 @@ export type ProductOption = Node & {
 };
 
 export type ProductOptionGroup = Node & {
-    __typename?: 'ProductOptionGroup';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2682,7 +2626,6 @@ export type ProductOptionGroup = Node & {
 };
 
 export type ProductOptionGroupTranslation = {
-    __typename?: 'ProductOptionGroupTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2698,7 +2641,6 @@ export type ProductOptionGroupTranslationInput = {
 };
 
 export type ProductOptionTranslation = {
-    __typename?: 'ProductOptionTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2723,7 +2665,6 @@ export type ProductSortParameter = {
 };
 
 export type ProductTranslation = {
-    __typename?: 'ProductTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2743,7 +2684,6 @@ export type ProductTranslationInput = {
 };
 
 export type ProductVariant = Node & {
-    __typename?: 'ProductVariant';
     enabled: Scalars['Boolean'];
     stockOnHand: Scalars['Int'];
     trackInventory: Scalars['Boolean'];
@@ -2790,7 +2730,6 @@ export type ProductVariantFilterParameter = {
 };
 
 export type ProductVariantList = PaginatedList & {
-    __typename?: 'ProductVariantList';
     items: Array<ProductVariant>;
     totalItems: Scalars['Int'];
 };
@@ -2815,7 +2754,6 @@ export type ProductVariantSortParameter = {
 };
 
 export type ProductVariantTranslation = {
-    __typename?: 'ProductVariantTranslation';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2831,7 +2769,6 @@ export type ProductVariantTranslationInput = {
 };
 
 export type Promotion = Node & {
-    __typename?: 'Promotion';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -2857,7 +2794,6 @@ export type PromotionFilterParameter = {
 };
 
 export type PromotionList = PaginatedList & {
-    __typename?: 'PromotionList';
     items: Array<Promotion>;
     totalItems: Scalars['Int'];
 };
@@ -2881,7 +2817,6 @@ export type PromotionSortParameter = {
 };
 
 export type Query = {
-    __typename?: 'Query';
     administrators: AdministratorList;
     administrator?: Maybe<Administrator>;
     /** Get a list of Assets */
@@ -3104,7 +3039,6 @@ export type QueryZoneArgs = {
 };
 
 export type Refund = Node & {
-    __typename?: 'Refund';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3136,7 +3070,6 @@ export type RemoveProductsFromChannelInput = {
 
 export type Return = Node &
     StockMovement & {
-        __typename?: 'Return';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3147,7 +3080,6 @@ export type Return = Node &
     };
 
 export type Role = Node & {
-    __typename?: 'Role';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3165,7 +3097,6 @@ export type RoleFilterParameter = {
 };
 
 export type RoleList = PaginatedList & {
-    __typename?: 'RoleList';
     items: Array<Role>;
     totalItems: Scalars['Int'];
 };
@@ -3187,7 +3118,6 @@ export type RoleSortParameter = {
 
 export type Sale = Node &
     StockMovement & {
-        __typename?: 'Sale';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3210,19 +3140,16 @@ export type SearchInput = {
 };
 
 export type SearchReindexResponse = {
-    __typename?: 'SearchReindexResponse';
     success: Scalars['Boolean'];
 };
 
 export type SearchResponse = {
-    __typename?: 'SearchResponse';
     items: Array<SearchResult>;
     totalItems: Scalars['Int'];
     facetValues: Array<FacetValueResult>;
 };
 
 export type SearchResult = {
-    __typename?: 'SearchResult';
     enabled: Scalars['Boolean'];
     /** An array of ids of the Collections in which this result appears */
     channelIds: Array<Scalars['ID']>;
@@ -3251,7 +3178,6 @@ export type SearchResult = {
 };
 
 export type SearchResultAsset = {
-    __typename?: 'SearchResultAsset';
     id: Scalars['ID'];
     preview: Scalars['String'];
     focalPoint?: Maybe<Coordinate>;
@@ -3266,7 +3192,6 @@ export type SearchResultSortParameter = {
 };
 
 export type ServerConfig = {
-    __typename?: 'ServerConfig';
     orderProcess: Array<OrderProcessState>;
     permittedAssetTypes: Array<Scalars['String']>;
     customFieldConfig: CustomFields;
@@ -3278,7 +3203,6 @@ export type SettleRefundInput = {
 };
 
 export type ShippingMethod = Node & {
-    __typename?: 'ShippingMethod';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3297,7 +3221,6 @@ export type ShippingMethodFilterParameter = {
 };
 
 export type ShippingMethodList = PaginatedList & {
-    __typename?: 'ShippingMethodList';
     items: Array<ShippingMethod>;
     totalItems: Scalars['Int'];
 };
@@ -3310,7 +3233,6 @@ export type ShippingMethodListOptions = {
 };
 
 export type ShippingMethodQuote = {
-    __typename?: 'ShippingMethodQuote';
     id: Scalars['ID'];
     price: Scalars['Int'];
     priceWithTax: Scalars['Int'];
@@ -3328,7 +3250,6 @@ export type ShippingMethodSortParameter = {
 
 /** The price value where the result has a single price */
 export type SinglePrice = {
-    __typename?: 'SinglePrice';
     value: Scalars['Int'];
 };
 
@@ -3339,7 +3260,6 @@ export enum SortOrder {
 
 export type StockAdjustment = Node &
     StockMovement & {
-        __typename?: 'StockAdjustment';
         id: Scalars['ID'];
         createdAt: Scalars['DateTime'];
         updatedAt: Scalars['DateTime'];
@@ -3360,7 +3280,6 @@ export type StockMovement = {
 export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
 
 export type StockMovementList = {
-    __typename?: 'StockMovementList';
     items: Array<StockMovementItem>;
     totalItems: Scalars['Int'];
 };
@@ -3379,7 +3298,6 @@ export enum StockMovementType {
 }
 
 export type StringCustomFieldConfig = CustomField & {
-    __typename?: 'StringCustomFieldConfig';
     name: Scalars['String'];
     type: Scalars['String'];
     list: Scalars['Boolean'];
@@ -3393,7 +3311,6 @@ export type StringCustomFieldConfig = CustomField & {
 };
 
 export type StringFieldOption = {
-    __typename?: 'StringFieldOption';
     value: Scalars['String'];
     label?: Maybe<Array<LocalizedString>>;
 };
@@ -3403,8 +3320,12 @@ export type StringOperators = {
     contains?: Maybe<Scalars['String']>;
 };
 
+/** Indicates that an operation succeeded, where we do not want to return any more specific information. */
+export type Success = {
+    success: Scalars['Boolean'];
+};
+
 export type TaxCategory = Node & {
-    __typename?: 'TaxCategory';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3412,7 +3333,6 @@ export type TaxCategory = Node & {
 };
 
 export type TaxRate = Node & {
-    __typename?: 'TaxRate';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3433,7 +3353,6 @@ export type TaxRateFilterParameter = {
 };
 
 export type TaxRateList = PaginatedList & {
-    __typename?: 'TaxRateList';
     items: Array<TaxRate>;
     totalItems: Scalars['Int'];
 };
@@ -3471,7 +3390,6 @@ export type TestShippingMethodOrderLineInput = {
 };
 
 export type TestShippingMethodQuote = {
-    __typename?: 'TestShippingMethodQuote';
     price: Scalars['Int'];
     priceWithTax: Scalars['Int'];
     description: Scalars['String'];
@@ -3479,11 +3397,12 @@ export type TestShippingMethodQuote = {
 };
 
 export type TestShippingMethodResult = {
-    __typename?: 'TestShippingMethodResult';
     eligible: Scalars['Boolean'];
     quote?: Maybe<TestShippingMethodQuote>;
 };
 
+export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
+
 export type UpdateAddressInput = {
     id: Scalars['ID'];
     fullName?: Maybe<Scalars['String']>;
@@ -3701,7 +3620,6 @@ export type UpdateZoneInput = {
 };
 
 export type User = Node & {
-    __typename?: 'User';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3714,7 +3632,6 @@ export type User = Node & {
 };
 
 export type Zone = Node & {
-    __typename?: 'Zone';
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
@@ -3726,95 +3643,70 @@ export type SearchProductsAdminQueryVariables = Exact<{
     input: SearchInput;
 }>;
 
-export type SearchProductsAdminQuery = { __typename?: 'Query' } & {
-    search: { __typename?: 'SearchResponse' } & Pick<SearchResponse, 'totalItems'> & {
-            items: Array<
-                { __typename?: 'SearchResult' } & Pick<
-                    SearchResult,
-                    | 'enabled'
-                    | 'productId'
-                    | 'productName'
-                    | 'productPreview'
-                    | 'productVariantId'
-                    | 'productVariantName'
-                    | 'productVariantPreview'
-                    | 'sku'
-                > & {
-                        productAsset?: Maybe<
-                            { __typename?: 'SearchResultAsset' } & Pick<
-                                SearchResultAsset,
-                                'id' | 'preview'
-                            > & {
-                                    focalPoint?: Maybe<
-                                        { __typename?: 'Coordinate' } & Pick<Coordinate, 'x' | 'y'>
-                                    >;
-                                }
-                        >;
-                        productVariantAsset?: Maybe<
-                            { __typename?: 'SearchResultAsset' } & Pick<
-                                SearchResultAsset,
-                                'id' | 'preview'
-                            > & {
-                                    focalPoint?: Maybe<
-                                        { __typename?: 'Coordinate' } & Pick<Coordinate, 'x' | 'y'>
-                                    >;
-                                }
-                        >;
+export type SearchProductsAdminQuery = {
+    search: Pick<SearchResponse, 'totalItems'> & {
+        items: Array<
+            Pick<
+                SearchResult,
+                | 'enabled'
+                | 'productId'
+                | 'productName'
+                | 'productPreview'
+                | 'productVariantId'
+                | 'productVariantName'
+                | 'productVariantPreview'
+                | 'sku'
+            > & {
+                productAsset?: Maybe<
+                    Pick<SearchResultAsset, 'id' | 'preview'> & {
+                        focalPoint?: Maybe<Pick<Coordinate, 'x' | 'y'>>;
+                    }
+                >;
+                productVariantAsset?: Maybe<
+                    Pick<SearchResultAsset, 'id' | 'preview'> & {
+                        focalPoint?: Maybe<Pick<Coordinate, 'x' | 'y'>>;
                     }
-            >;
-        };
+                >;
+            }
+        >;
+    };
 };
 
 export type SearchFacetValuesQueryVariables = Exact<{
     input: SearchInput;
 }>;
 
-export type SearchFacetValuesQuery = { __typename?: 'Query' } & {
-    search: { __typename?: 'SearchResponse' } & Pick<SearchResponse, 'totalItems'> & {
-            facetValues: Array<
-                { __typename?: 'FacetValueResult' } & Pick<FacetValueResult, 'count'> & {
-                        facetValue: { __typename?: 'FacetValue' } & Pick<FacetValue, 'id' | 'name'>;
-                    }
-            >;
-        };
+export type SearchFacetValuesQuery = {
+    search: Pick<SearchResponse, 'totalItems'> & {
+        facetValues: Array<Pick<FacetValueResult, 'count'> & { facetValue: Pick<FacetValue, 'id' | 'name'> }>;
+    };
 };
 
 export type SearchGetPricesQueryVariables = Exact<{
     input: SearchInput;
 }>;
 
-export type SearchGetPricesQuery = { __typename?: 'Query' } & {
-    search: { __typename?: 'SearchResponse' } & {
-        items: Array<
-            { __typename?: 'SearchResult' } & {
-                price:
-                    | ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
-                    | ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>);
-                priceWithTax:
-                    | ({ __typename?: 'PriceRange' } & Pick<PriceRange, 'min' | 'max'>)
-                    | ({ __typename?: 'SinglePrice' } & Pick<SinglePrice, 'value'>);
-            }
-        >;
+export type SearchGetPricesQuery = {
+    search: {
+        items: Array<{
+            price: Pick<PriceRange, 'min' | 'max'> | Pick<SinglePrice, 'value'>;
+            priceWithTax: Pick<PriceRange, 'min' | 'max'> | Pick<SinglePrice, 'value'>;
+        }>;
     };
 };
 
 export type ReindexMutationVariables = Exact<{ [key: string]: never }>;
 
-export type ReindexMutation = { __typename?: 'Mutation' } & {
-    reindex: { __typename?: 'Job' } & Pick<
-        Job,
-        'id' | 'queueName' | 'state' | 'progress' | 'duration' | 'result'
-    >;
+export type ReindexMutation = {
+    reindex: Pick<Job, 'id' | 'queueName' | 'state' | 'progress' | 'duration' | 'result'>;
 };
 
 export type GetJobInfoQueryVariables = Exact<{
     id: Scalars['ID'];
 }>;
 
-export type GetJobInfoQuery = { __typename?: 'Query' } & {
-    job?: Maybe<
-        { __typename?: 'Job' } & Pick<Job, 'id' | 'queueName' | 'state' | 'progress' | 'duration' | 'result'>
-    >;
+export type GetJobInfoQuery = {
+    job?: Maybe<Pick<Job, 'id' | 'queueName' | 'state' | 'progress' | 'duration' | 'result'>>;
 };
 
 type DiscriminateUnion<T, U> = T extends U ? T : never;

ファイルの差分が大きいため隠しています
+ 0 - 0
schema-admin.json


ファイルの差分が大きいため隠しています
+ 0 - 0
schema-shop.json


+ 28 - 24
scripts/codegen/generate-graphql-types.ts

@@ -50,6 +50,10 @@ Promise.all([
             },
             strict: true,
         };
+        const e2eConfig = {
+            ...config,
+            skipTypename: true,
+        };
         const disableTsLintPlugin = { add: { content: '// tslint:disable' } };
         const graphQlErrorsPlugin = path.join(__dirname, './plugins/graphql-errors-plugin.js');
         const commonPlugins = [disableTsLintPlugin, 'typescript'];
@@ -72,17 +76,17 @@ Promise.all([
                     schema: [SHOP_SCHEMA_OUTPUT_FILE],
                     plugins: [disableTsLintPlugin, graphQlErrorsPlugin],
                 },
-                /*[path.join(__dirname, '../../packages/core/e2e/graphql/generated-e2e-admin-types.ts')]: {
+                [path.join(__dirname, '../../packages/core/e2e/graphql/generated-e2e-admin-types.ts')]: {
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE],
                     documents: E2E_ADMIN_QUERY_FILES,
                     plugins: clientPlugins,
-                    config,
+                    config: e2eConfig,
                 },
                 [path.join(__dirname, '../../packages/core/e2e/graphql/generated-e2e-shop-types.ts')]: {
                     schema: [SHOP_SCHEMA_OUTPUT_FILE],
                     documents: E2E_SHOP_QUERY_FILES,
                     plugins: clientPlugins,
-                    config,
+                    config: e2eConfig,
                 },
                 [path.join(
                     __dirname,
@@ -91,7 +95,7 @@ Promise.all([
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE],
                     documents: E2E_ELASTICSEARCH_PLUGIN_QUERY_FILES,
                     plugins: clientPlugins,
-                    config,
+                    config: e2eConfig,
                 },
                 [path.join(
                     __dirname,
@@ -100,26 +104,26 @@ Promise.all([
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE],
                     documents: E2E_ASSET_SERVER_PLUGIN_QUERY_FILES,
                     plugins: clientPlugins,
-                    config,
-                },
-                [path.join(
-                    __dirname,
-                    '../../packages/admin-ui/src/lib/core/src/common/generated-types.ts',
-                )]: {
-                    schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
-                    documents: CLIENT_QUERY_FILES,
-                    plugins: clientPlugins,
-                    config,
-                },
-                [path.join(
-                    __dirname,
-                    '../../packages/admin-ui/src/lib/core/src/common/introspection-result.ts',
-                )]: {
-                    schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
-                    documents: CLIENT_QUERY_FILES,
-                    plugins: [disableTsLintPlugin, 'fragment-matcher'],
-                    config,
+                    config: e2eConfig,
                 },
+                // [path.join(
+                //     __dirname,
+                //     '../../packages/admin-ui/src/lib/core/src/common/generated-types.ts',
+                // )]: {
+                //     schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
+                //     documents: CLIENT_QUERY_FILES,
+                //     plugins: clientPlugins,
+                //     config,
+                // },
+                // [path.join(
+                //     __dirname,
+                //     '../../packages/admin-ui/src/lib/core/src/common/introspection-result.ts',
+                // )]: {
+                //     schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
+                //     documents: CLIENT_QUERY_FILES,
+                //     plugins: [disableTsLintPlugin, 'fragment-matcher'],
+                //     config,
+                // },
                 [path.join(__dirname, '../../packages/common/src/generated-types.ts')]: {
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE],
                     plugins: commonPlugins,
@@ -141,7 +145,7 @@ Promise.all([
                         },
                         maybeValue: 'T',
                     },
-                },*/
+                },
             },
         });
     })

+ 40 - 16
scripts/codegen/plugins/graphql-errors-plugin.ts

@@ -68,9 +68,12 @@ const errorsVisitor: Visitor<any> = {
         return [
             `export class ${node.name.value} extends ${ERROR_INTERFACE_NAME} {`,
             `  readonly __typename = '${node.name.value}';`,
-            `  readonly code = ErrorCode.${node.name.value};`,
+            `  readonly code = ErrorCode.${camelToUpperSnakeCase(node.name.value)};`,
+            `  readonly message = '${camelToUpperSnakeCase(node.name.value)}';`,
             `  constructor(`,
-            ...node.fields.filter(f => !(f as any).includes('code:')).map(f => `    public ${f},`),
+            ...node.fields
+                .filter(f => !(f as any).includes('code:') && !(f as any).includes('message:'))
+                .map(f => `    public ${f},`),
             `  ) {`,
             `    super();`,
             `  }`,
@@ -83,11 +86,14 @@ export const plugin: PluginFunction<any> = (schema, documents, config, info) =>
     const printedSchema = printSchema(schema); // Returns a string representation of the schema
     const astNode = parse(printedSchema); // Transforms the string into ASTNode
     const result = visit(astNode, { leave: errorsVisitor });
-    const defs = result.definitions.filter(d => !!d);
+    const defs = result.definitions
+        .filter(d => !!d)
+        // Ensure the ErrorResult base class is first
+        .sort((a, b) => (a.includes('class ErrorResult') ? -1 : 1));
     return {
         content: [
             `/** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */`,
-            `import { ErrorCode } from '@vendure/common/lib/generated-types';`,
+            generateErrorCodeImport(schema),
             generateScalars(schema, config),
             ...defs,
             defs.length ? generateIsErrorFunction(schema) : '',
@@ -96,6 +102,11 @@ export const plugin: PluginFunction<any> = (schema, documents, config, info) =>
     };
 };
 
+function generateErrorCodeImport(schema: GraphQLSchema): string {
+    const typesFile = isAdminApi(schema) ? `generated-types` : `generated-shop-types`;
+    return `import { ErrorCode } from '@vendure/common/lib/${typesFile}';`;
+}
+
 function generateScalars(schema: GraphQLSchema, config: any): string {
     const scalarMap = buildScalars(schema, config.scalars);
     const allScalars = Object.keys(scalarMap)
@@ -123,7 +134,7 @@ function generateIsErrorFunction(schema: GraphQLSchema) {
         .filter(node => inheritsFromErrorResult(node));
     return `
 const errorTypeNames = new Set([${errorNodes.map(n => `'${n.name.value}'`).join(', ')}]);
-export function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').${ERROR_INTERFACE_NAME} {
+function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').${ERROR_INTERFACE_NAME} {
   return input instanceof ${ERROR_INTERFACE_NAME} || errorTypeNames.has(input.__typename);
 }`;
 }
@@ -132,20 +143,25 @@ function generateTypeResolvers(schema: GraphQLSchema) {
     const mutations = getOperationsThatReturnErrorUnions(schema, schema.getMutationType().getFields());
     const queries = getOperationsThatReturnErrorUnions(schema, schema.getQueryType().getFields());
     const operations = [...mutations, ...queries];
-    const isAdminApi = !!schema.getType('UpdateGlobalSettingsInput');
-    const varName = isAdminApi ? `adminErrorOperationTypeResolvers` : `shopErrorOperationTypeResolvers`;
+    const varName = isAdminApi(schema)
+        ? `adminErrorOperationTypeResolvers`
+        : `shopErrorOperationTypeResolvers`;
     const result = [`export const ${varName} = {`];
+    const typesHandled = new Set<string>();
     for (const operation of operations) {
         const returnType = unwrapType(operation.type) as GraphQLUnionType;
-        const nonErrorResult = returnType.getTypes().find(t => !inheritsFromErrorResult(t));
-        result.push(
-            `  ${returnType.name}: {`,
-            `    __resolveType(value: any) {`,
-            // tslint:disable-next-line:no-non-null-assertion
-            `      return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult!.name}';`,
-            `    },`,
-            `  },`,
-        );
+        if (!typesHandled.has(returnType.name)) {
+            typesHandled.add(returnType.name);
+            const nonErrorResult = returnType.getTypes().find(t => !inheritsFromErrorResult(t));
+            result.push(
+                `  ${returnType.name}: {`,
+                `    __resolveType(value: any) {`,
+                // tslint:disable-next-line:no-non-null-assertion
+                `      return isGraphQLError(value) ? (value as any).__typename : '${nonErrorResult!.name}';`,
+                `    },`,
+                `  },`,
+            );
+        }
     }
     result.push(`};`);
     return result.join('\n');
@@ -198,3 +214,11 @@ function unwrapType(type: GraphQLType): GraphQLNamedType {
     }
     return innerType;
 }
+
+function isAdminApi(schema: GraphQLSchema): boolean {
+    return !!schema.getType('UpdateGlobalSettingsInput');
+}
+
+function camelToUpperSnakeCase(input: string): string {
+    return input.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase();
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません