Browse Source

feat(core): Improved error handling for Admin API mutations

Relates to #437

feat(core): Improved error handling for remaining admin mutations
feat(core): Improved error handling for admin promotion mutations
feat(core): Improved error handling for admin order mutations
feat(core): Improved error handling for global settings mutations
feat(core): Improved error handling for customer mutations
feat(core): Improved error handling auth mutations
Michael Bromley 5 years ago
parent
commit
af49054172
66 changed files with 3131 additions and 1028 deletions
  1. 231 22
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  2. 10 11
      packages/common/src/generated-shop-types.ts
  3. 230 24
      packages/common/src/generated-types.ts
  4. 10 10
      packages/core/e2e/auth.e2e-spec.ts
  5. 32 18
      packages/core/e2e/authentication-strategy.e2e-spec.ts
  6. 65 34
      packages/core/e2e/channel.e2e-spec.ts
  7. 3 1
      packages/core/e2e/custom-fields.e2e-spec.ts
  8. 59 16
      packages/core/e2e/customer.e2e-spec.ts
  9. 149 0
      packages/core/e2e/global-settings.e2e-spec.ts
  10. 30 0
      packages/core/e2e/graphql/fragments.ts
  11. 441 82
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  12. 10 10
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  13. 43 27
      packages/core/e2e/graphql/shared-definitions.ts
  14. 14 5
      packages/core/e2e/order-promotion.e2e-spec.ts
  15. 363 241
      packages/core/e2e/order.e2e-spec.ts
  16. 49 32
      packages/core/e2e/product.e2e-spec.ts
  17. 113 87
      packages/core/e2e/promotion.e2e-spec.ts
  18. 3 1
      packages/core/e2e/session-management.e2e-spec.ts
  19. 9 19
      packages/core/e2e/shop-auth.e2e-spec.ts
  20. 1 1
      packages/core/e2e/shop-customer.e2e-spec.ts
  21. 1 1
      packages/core/src/api/middleware/translate-error-result-interceptor.ts
  22. 19 8
      packages/core/src/api/resolvers/admin/auth.resolver.ts
  23. 17 7
      packages/core/src/api/resolvers/admin/channel.resolver.ts
  24. 5 3
      packages/core/src/api/resolvers/admin/customer.resolver.ts
  25. 12 5
      packages/core/src/api/resolvers/admin/global-settings.resolver.ts
  26. 25 5
      packages/core/src/api/resolvers/admin/order.resolver.ts
  27. 3 1
      packages/core/src/api/resolvers/admin/product.resolver.ts
  28. 14 7
      packages/core/src/api/resolvers/admin/promotion.resolver.ts
  29. 43 12
      packages/core/src/api/resolvers/base/base-auth.resolver.ts
  30. 31 35
      packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
  31. 5 2
      packages/core/src/api/resolvers/shop/shop-customer.resolver.ts
  32. 6 3
      packages/core/src/api/schema/admin-api/auth.api.graphql
  33. 12 2
      packages/core/src/api/schema/admin-api/channel.api.graphql
  34. 5 2
      packages/core/src/api/schema/admin-api/customer.api.graphql
  35. 14 1
      packages/core/src/api/schema/admin-api/global-settings.api.graphql
  36. 118 6
      packages/core/src/api/schema/admin-api/order.api.graphql
  37. 10 1
      packages/core/src/api/schema/admin-api/product.api.graphql
  38. 11 2
      packages/core/src/api/schema/admin-api/promotion.api.graphql
  39. 18 0
      packages/core/src/api/schema/common/common-types.graphql
  40. 5 21
      packages/core/src/api/schema/shop-api/shop.api.graphql
  41. 0 4
      packages/core/src/api/schema/type/auth.type.graphql
  42. 1 1
      packages/core/src/common/error/error-result.ts
  43. 302 6
      packages/core/src/common/error/generated-graphql-admin-errors.ts
  44. 34 26
      packages/core/src/common/error/generated-graphql-shop-errors.ts
  45. 1 0
      packages/core/src/common/index.ts
  46. 5 6
      packages/core/src/config/auth/native-authentication-strategy.ts
  47. 15 28
      packages/core/src/i18n/messages/en.json
  48. 6 5
      packages/core/src/service/services/auth.service.ts
  49. 34 17
      packages/core/src/service/services/channel.service.ts
  50. 59 28
      packages/core/src/service/services/customer.service.ts
  51. 2 1
      packages/core/src/service/services/history.service.ts
  52. 97 60
      packages/core/src/service/services/order.service.ts
  53. 7 2
      packages/core/src/service/services/payment-method.service.ts
  54. 5 5
      packages/core/src/service/services/product.service.ts
  55. 18 14
      packages/core/src/service/services/promotion.service.ts
  56. 1 1
      packages/core/src/service/services/tax-category.service.ts
  57. 20 0
      packages/dev-server/dev-config.ts
  58. 231 22
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  59. 5 5
      packages/elasticsearch-plugin/src/elasticsearch.service.ts
  60. 8 8
      packages/elasticsearch-plugin/src/indexer.controller.ts
  61. 5 5
      packages/elasticsearch-plugin/src/types.ts
  62. 8 3
      packages/testing/src/data-population/mock-data.service.ts
  63. 13 5
      packages/testing/src/simple-graphql-client.ts
  64. 0 0
      schema-admin.json
  65. 0 0
      schema-shop.json
  66. 15 11
      scripts/codegen/plugins/graphql-errors-plugin.ts

+ 231 - 22
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -16,6 +16,11 @@ export type Scalars = {
     Upload: any;
     Upload: any;
 };
 };
 
 
+export type AddFulfillmentToOrderResult =
+    | Fulfillment
+    | EmptyOrderLineSelectionError
+    | ItemsAlreadyFulfilledError;
+
 export type AddNoteToCustomerInput = {
 export type AddNoteToCustomerInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     note: Scalars['String'];
     note: Scalars['String'];
@@ -102,6 +107,13 @@ export type AdministratorSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     emailAddress?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund an OrderItem which has already been refunded */
+export type AlreadyRefundedError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    refundId: Scalars['ID'];
+};
+
 export type Asset = Node & {
 export type Asset = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -178,6 +190,8 @@ export type AuthenticationMethod = Node & {
     strategy: Scalars['String'];
     strategy: Scalars['String'];
 };
 };
 
 
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
     name: Scalars['String'];
     name: Scalars['String'];
     type: Scalars['String'];
     type: Scalars['String'];
@@ -192,6 +206,13 @@ export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
 };
 };
 
 
+/** Returned if an attempting to cancel lines from an Order which is still active */
+export type CancelActiveOrderError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    orderState: Scalars['String'];
+};
+
 export type Cancellation = Node &
 export type Cancellation = Node &
     StockMovement & {
     StockMovement & {
         id: Scalars['ID'];
         id: Scalars['ID'];
@@ -211,6 +232,14 @@ export type CancelOrderInput = {
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type CancelOrderResult =
+    | Order
+    | EmptyOrderLineSelectionError
+    | QuantityTooGreatError
+    | MultipleOrderError
+    | CancelActiveOrderError
+    | OrderStateTransitionError;
+
 export type Channel = Node & {
 export type Channel = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -224,6 +253,17 @@ export type Channel = Node & {
     pricesIncludeTax: Scalars['Boolean'];
     pricesIncludeTax: Scalars['Boolean'];
 };
 };
 
 
+/**
+ * Returned when the default LanguageCode of a Channel is no longer found in the `availableLanguages`
+ * of the GlobalSettings
+ */
+export type ChannelDefaultLanguageError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    language: Scalars['String'];
+    channelCode: Scalars['String'];
+};
+
 export type Collection = Node & {
 export type Collection = Node & {
     isPrivate: Scalars['Boolean'];
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -436,6 +476,8 @@ export type CreateChannelInput = {
     defaultShippingZoneId: Scalars['ID'];
     defaultShippingZoneId: Scalars['ID'];
 };
 };
 
 
+export type CreateChannelResult = Channel | LanguageNotAvailableError;
+
 export type CreateCollectionInput = {
 export type CreateCollectionInput = {
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
     featuredAssetId?: Maybe<Scalars['ID']>;
     featuredAssetId?: Maybe<Scalars['ID']>;
@@ -474,6 +516,8 @@ export type CreateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type CreateCustomerResult = Customer | EmailAddressConflictError;
+
 export type CreateFacetInput = {
 export type CreateFacetInput = {
     code: Scalars['String'];
     code: Scalars['String'];
     isPrivate: Scalars['Boolean'];
     isPrivate: Scalars['Boolean'];
@@ -553,6 +597,8 @@ export type CreatePromotionInput = {
     actions: Array<ConfigurableOperationInput>;
     actions: Array<ConfigurableOperationInput>;
 };
 };
 
 
+export type CreatePromotionResult = Promotion | MissingConditionsError;
+
 export type CreateRoleInput = {
 export type CreateRoleInput = {
     code: Scalars['String'];
     code: Scalars['String'];
     description: Scalars['String'];
     description: Scalars['String'];
@@ -1093,10 +1139,42 @@ export enum DeletionResult {
     NOT_DELETED = 'NOT_DELETED',
     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 & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned if no OrderLines have been specified for the operation */
+export type EmptyOrderLineSelectionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export enum ErrorCode {
 export enum ErrorCode {
     UNKNOWN_ERROR = 'UNKNOWN_ERROR',
     UNKNOWN_ERROR = 'UNKNOWN_ERROR',
     MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
     MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
+    INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
+    NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+    LANGUAGE_NOT_AVAILABLE_ERROR = 'LANGUAGE_NOT_AVAILABLE_ERROR',
+    EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+    CHANNEL_DEFAULT_LANGUAGE_ERROR = 'CHANNEL_DEFAULT_LANGUAGE_ERROR',
+    SETTLE_PAYMENT_ERROR = 'SETTLE_PAYMENT_ERROR',
+    PAYMENT_STATE_TRANSITION_ERROR = 'PAYMENT_STATE_TRANSITION_ERROR',
     ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
     ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
+    EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
+    ITEMS_ALREADY_FULFILLED_ERROR = 'ITEMS_ALREADY_FULFILLED_ERROR',
+    QUANTITY_TOO_GREAT_ERROR = 'QUANTITY_TOO_GREAT_ERROR',
+    MULTIPLE_ORDER_ERROR = 'MULTIPLE_ORDER_ERROR',
+    CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
+    NOTHING_TO_REFUND_ERROR = 'NOTHING_TO_REFUND_ERROR',
+    PAYMENT_ORDER_MISMATCH_ERROR = 'PAYMENT_ORDER_MISMATCH_ERROR',
+    REFUND_ORDER_STATE_ERROR = 'REFUND_ORDER_STATE_ERROR',
+    ALREADY_REFUNDED_ERROR = 'ALREADY_REFUNDED_ERROR',
+    REFUND_STATE_TRANSITION_ERROR = 'REFUND_STATE_TRANSITION_ERROR',
+    FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+    PRODUCT_OPTION_IN_USE_ERROR = 'PRODUCT_OPTION_IN_USE_ERROR',
+    MISSING_CONDITIONS_ERROR = 'MISSING_CONDITIONS_ERROR',
 }
 }
 
 
 export type ErrorResult = {
 export type ErrorResult = {
@@ -1221,6 +1299,15 @@ export type Fulfillment = Node & {
     trackingCode?: Maybe<Scalars['String']>;
     trackingCode?: Maybe<Scalars['String']>;
 };
 };
 
 
+/** Returned when there is an error in transitioning the Fulfillment state */
+export type FulfillmentStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 export type FulfillOrderInput = {
 export type FulfillOrderInput = {
     lines: Array<OrderLineInput>;
     lines: Array<OrderLineInput>;
     method: Scalars['String'];
     method: Scalars['String'];
@@ -1317,6 +1404,18 @@ export type IntCustomFieldConfig = CustomField & {
     step?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
 };
 };
 
 
+/** Returned if the user authentication credentials are not valid */
+export type InvalidCredentialsError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned if the specified items are already part of a Fulfillment */
+export type ItemsAlreadyFulfilledError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Job = Node & {
 export type Job = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -1710,6 +1809,12 @@ export enum LanguageCode {
     zu = 'zu',
     zu = 'zu',
 }
 }
 
 
+export type LanguageNotAvailableError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    languageCode: Scalars['String'];
+};
+
 export type LocaleStringCustomFieldConfig = CustomField & {
 export type LocaleStringCustomFieldConfig = CustomField & {
     name: Scalars['String'];
     name: Scalars['String'];
     type: Scalars['String'];
     type: Scalars['String'];
@@ -1732,10 +1837,6 @@ export enum LogicalOperator {
     OR = 'OR',
     OR = 'OR',
 }
 }
 
 
-export type LoginResult = {
-    user: CurrentUser;
-};
-
 export type MimeTypeError = ErrorResult & {
 export type MimeTypeError = ErrorResult & {
     code: ErrorCode;
     code: ErrorCode;
     message: Scalars['String'];
     message: Scalars['String'];
@@ -1743,12 +1844,24 @@ export type MimeTypeError = ErrorResult & {
     mimeType: Scalars['String'];
     mimeType: Scalars['String'];
 };
 };
 
 
+/** Returned if a PromotionCondition has neither a couponCode nor any conditions set */
+export type MissingConditionsError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type MoveCollectionInput = {
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
     collectionId: Scalars['ID'];
     parentId: Scalars['ID'];
     parentId: Scalars['ID'];
     index: Scalars['Int'];
     index: Scalars['Int'];
 };
 };
 
 
+/** Returned if an operation has specified OrderLines from multiple Orders */
+export type MultipleOrderError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Mutation = {
 export type Mutation = {
     /** Create a new Administrator */
     /** Create a new Administrator */
     createAdministrator: Administrator;
     createAdministrator: Administrator;
@@ -1770,14 +1883,14 @@ export type Mutation = {
      * Authenticates the user using the native authentication strategy. This mutation
      * Authenticates the user using the native authentication strategy. This mutation
      * is an alias for `authenticate({ native: { ... }})`
      * is an alias for `authenticate({ native: { ... }})`
      */
      */
-    login: LoginResult;
+    login: NativeAuthenticationResult;
     /** Authenticates the user using a named authentication strategy */
     /** Authenticates the user using a named authentication strategy */
-    authenticate: LoginResult;
-    logout: Scalars['Boolean'];
+    authenticate: AuthenticationResult;
+    logout: Success;
     /** Create a new Channel */
     /** Create a new Channel */
-    createChannel: Channel;
+    createChannel: CreateChannelResult;
     /** Update an existing Channel */
     /** Update an existing Channel */
-    updateChannel: Channel;
+    updateChannel: UpdateChannelResult;
     /** Delete a Channel */
     /** Delete a Channel */
     deleteChannel: DeletionResponse;
     deleteChannel: DeletionResponse;
     /** Create a new Collection */
     /** Create a new Collection */
@@ -1805,9 +1918,9 @@ export type Mutation = {
     /** Remove Customers from a CustomerGroup */
     /** Remove Customers from a CustomerGroup */
     removeCustomersFromGroup: CustomerGroup;
     removeCustomersFromGroup: CustomerGroup;
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
-    createCustomer: Customer;
+    createCustomer: CreateCustomerResult;
     /** Update an existing Customer */
     /** Update an existing Customer */
-    updateCustomer: Customer;
+    updateCustomer: UpdateCustomerResult;
     /** Delete a Customer */
     /** Delete a Customer */
     deleteCustomer: DeletionResponse;
     deleteCustomer: DeletionResponse;
     /** Create a new Address and associate it with the Customer specified by customerId */
     /** Create a new Address and associate it with the Customer specified by customerId */
@@ -1831,20 +1944,20 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
     deleteFacetValues: Array<DeletionResponse>;
-    updateGlobalSettings: GlobalSettings;
+    updateGlobalSettings: UpdateGlobalSettingsResult;
     importProducts?: Maybe<ImportInfo>;
     importProducts?: Maybe<ImportInfo>;
     /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
     /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
     removeSettledJobs: Scalars['Int'];
     removeSettledJobs: Scalars['Int'];
-    settlePayment: Payment;
-    fulfillOrder: Fulfillment;
-    cancelOrder: Order;
-    refundOrder: Refund;
-    settleRefund: Refund;
+    settlePayment: SettlePaymentResult;
+    addFulfillmentToOrder: AddFulfillmentToOrderResult;
+    cancelOrder: CancelOrderResult;
+    refundOrder: RefundOrderResult;
+    settleRefund: SettleRefundResult;
     addNoteToOrder: Order;
     addNoteToOrder: Order;
     updateOrderNote: HistoryEntry;
     updateOrderNote: HistoryEntry;
     deleteOrderNote: DeletionResponse;
     deleteOrderNote: DeletionResponse;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
-    transitionFulfillmentToState: Fulfillment;
+    transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     setOrderCustomFields?: Maybe<Order>;
     setOrderCustomFields?: Maybe<Order>;
     /** Update an existing PaymentMethod */
     /** Update an existing PaymentMethod */
     updatePaymentMethod: PaymentMethod;
     updatePaymentMethod: PaymentMethod;
@@ -1866,7 +1979,7 @@ export type Mutation = {
     /** Add an OptionGroup to a Product */
     /** Add an OptionGroup to a Product */
     addOptionGroupToProduct: Product;
     addOptionGroupToProduct: Product;
     /** Remove an OptionGroup from a Product */
     /** Remove an OptionGroup from a Product */
-    removeOptionGroupFromProduct: Product;
+    removeOptionGroupFromProduct: RemoveOptionGroupFromProductResult;
     /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
     /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
     createProductVariants: Array<Maybe<ProductVariant>>;
     createProductVariants: Array<Maybe<ProductVariant>>;
     /** Update existing ProductVariants */
     /** Update existing ProductVariants */
@@ -1877,8 +1990,8 @@ export type Mutation = {
     assignProductsToChannel: Array<Product>;
     assignProductsToChannel: Array<Product>;
     /** Removes Products from the specified Channel */
     /** Removes Products from the specified Channel */
     removeProductsFromChannel: Array<Product>;
     removeProductsFromChannel: Array<Product>;
-    createPromotion: Promotion;
-    updatePromotion: Promotion;
+    createPromotion: CreatePromotionResult;
+    updatePromotion: UpdatePromotionResult;
     deletePromotion: DeletionResponse;
     deletePromotion: DeletionResponse;
     /** Create a new Role */
     /** Create a new Role */
     createRole: Role;
     createRole: Role;
@@ -2105,7 +2218,7 @@ export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
-export type MutationFulfillOrderArgs = {
+export type MutationAddFulfillmentToOrderArgs = {
     input: FulfillOrderInput;
     input: FulfillOrderInput;
 };
 };
 
 
@@ -2291,15 +2404,29 @@ export type MutationRemoveMembersFromZoneArgs = {
     memberIds: Array<Scalars['ID']>;
     memberIds: Array<Scalars['ID']>;
 };
 };
 
 
+export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type NativeAuthInput = {
 export type NativeAuthInput = {
     username: Scalars['String'];
     username: Scalars['String'];
     password: Scalars['String'];
     password: Scalars['String'];
 };
 };
 
 
+/** Retured when attempting an operation that relies on the NativeAuthStrategy, if that strategy is not configured. */
+export type NativeAuthStrategyError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Node = {
 export type Node = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
+/** Returned if an attempting to refund an Order but neither items nor shipping refund was specified */
+export type NothingToRefundError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
     lt?: Maybe<Scalars['Float']>;
     lt?: Maybe<Scalars['Float']>;
@@ -2509,6 +2636,21 @@ export type PaymentMethodSortParameter = {
     code?: Maybe<SortOrder>;
     code?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund a Payment against OrderLines from a different Order */
+export type PaymentOrderMismatchError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Payment state */
+export type PaymentStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 /**
 /**
  * "
  * "
  * @description
  * @description
@@ -2640,6 +2782,13 @@ export type ProductOptionGroupTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type ProductOptionInUseError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    optionGroupCode: Scalars['String'];
+    productVariantCount: Scalars['Int'];
+};
+
 export type ProductOptionTranslation = {
 export type ProductOptionTranslation = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -2816,6 +2965,12 @@ export type PromotionSortParameter = {
     name?: Maybe<SortOrder>;
     name?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
+export type QuantityTooGreatError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Query = {
 export type Query = {
     administrators: AdministratorList;
     administrators: AdministratorList;
     administrator?: Maybe<Administrator>;
     administrator?: Maybe<Administrator>;
@@ -3063,6 +3218,35 @@ export type RefundOrderInput = {
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type RefundOrderResult =
+    | Refund
+    | QuantityTooGreatError
+    | NothingToRefundError
+    | OrderStateTransitionError
+    | MultipleOrderError
+    | PaymentOrderMismatchError
+    | RefundOrderStateError
+    | AlreadyRefundedError
+    | RefundStateTransitionError;
+
+/** Returned if an attempting to refund an Order which is not in the expected state */
+export type RefundOrderStateError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    orderState: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Refund state */
+export type RefundStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
+export type RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError;
+
 export type RemoveProductsFromChannelInput = {
 export type RemoveProductsFromChannelInput = {
     productIds: Array<Scalars['ID']>;
     productIds: Array<Scalars['ID']>;
     channelId: Scalars['ID'];
     channelId: Scalars['ID'];
@@ -3197,11 +3381,26 @@ export type ServerConfig = {
     customFieldConfig: CustomFields;
     customFieldConfig: CustomFields;
 };
 };
 
 
+/** Returned if the Payment settlement fails */
+export type SettlePaymentError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    paymentErrorMessage: Scalars['String'];
+};
+
+export type SettlePaymentResult =
+    | Payment
+    | SettlePaymentError
+    | PaymentStateTransitionError
+    | OrderStateTransitionError;
+
 export type SettleRefundInput = {
 export type SettleRefundInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     transactionId: Scalars['String'];
     transactionId: Scalars['String'];
 };
 };
 
 
+export type SettleRefundResult = Refund | RefundStateTransitionError;
+
 export type ShippingMethod = Node & {
 export type ShippingMethod = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -3401,6 +3600,8 @@ export type TestShippingMethodResult = {
     quote?: Maybe<TestShippingMethodQuote>;
     quote?: Maybe<TestShippingMethodQuote>;
 };
 };
 
 
+export type TransitionFulfillmentToStateResult = Fulfillment | FulfillmentStateTransitionError;
+
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 
 
 export type UpdateAddressInput = {
 export type UpdateAddressInput = {
@@ -3445,6 +3646,8 @@ export type UpdateChannelInput = {
     defaultShippingZoneId?: Maybe<Scalars['ID']>;
     defaultShippingZoneId?: Maybe<Scalars['ID']>;
 };
 };
 
 
+export type UpdateChannelResult = Channel | LanguageNotAvailableError;
+
 export type UpdateCollectionInput = {
 export type UpdateCollectionInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3492,6 +3695,8 @@ export type UpdateCustomerNoteInput = {
     note: Scalars['String'];
     note: Scalars['String'];
 };
 };
 
 
+export type UpdateCustomerResult = Customer | EmailAddressConflictError;
+
 export type UpdateFacetInput = {
 export type UpdateFacetInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3513,6 +3718,8 @@ export type UpdateGlobalSettingsInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type UpdateGlobalSettingsResult = GlobalSettings | ChannelDefaultLanguageError;
+
 export type UpdateOrderInput = {
 export type UpdateOrderInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
@@ -3582,6 +3789,8 @@ export type UpdatePromotionInput = {
     actions?: Maybe<Array<ConfigurableOperationInput>>;
     actions?: Maybe<Array<ConfigurableOperationInput>>;
 };
 };
 
 
+export type UpdatePromotionResult = Promotion | MissingConditionsError;
+
 export type UpdateRoleInput = {
 export type UpdateRoleInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     code?: Maybe<Scalars['String']>;
     code?: Maybe<Scalars['String']>;

+ 10 - 11
packages/common/src/generated-shop-types.ts

@@ -130,6 +130,8 @@ export type AuthenticationMethod = Node & {
     strategy: Scalars['String'];
     strategy: Scalars['String'];
 };
 };
 
 
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
     __typename?: 'BooleanCustomFieldConfig';
     __typename?: 'BooleanCustomFieldConfig';
     name: Scalars['String'];
     name: Scalars['String'];
@@ -873,12 +875,12 @@ export enum ErrorCode {
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
     EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
-    MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
     NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
     NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+    MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_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_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
@@ -1090,7 +1092,7 @@ export type IntCustomFieldConfig = CustomField & {
     step?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
 };
 };
 
 
-/** Returned if the user credentials are not valid */
+/** Returned if the user authentication credentials are not valid */
 export type InvalidCredentialsError = ErrorResult & {
 export type InvalidCredentialsError = ErrorResult & {
     __typename?: 'InvalidCredentialsError';
     __typename?: 'InvalidCredentialsError';
     code: ErrorCode;
     code: ErrorCode;
@@ -1447,11 +1449,6 @@ export enum LogicalOperator {
     OR = 'OR',
     OR = 'OR',
 }
 }
 
 
-export type LoginResult = {
-    __typename?: 'LoginResult';
-    user: CurrentUser;
-};
-
 /** Retured when attemting to register or verify a customer account without a password, when one is required. */
 /** Retured when attemting to register or verify a customer account without a password, when one is required. */
 export type MissingPasswordError = ErrorResult & {
 export type MissingPasswordError = ErrorResult & {
     __typename?: 'MissingPasswordError';
     __typename?: 'MissingPasswordError';
@@ -1497,11 +1494,11 @@ export type Mutation = {
      * Authenticates the user using the native authentication strategy. This mutation
      * Authenticates the user using the native authentication strategy. This mutation
      * is an alias for `authenticate({ native: { ... }})`
      * is an alias for `authenticate({ native: { ... }})`
      */
      */
-    login: LoginResult;
+    login: NativeAuthenticationResult;
     /** Authenticates the user using a named authentication strategy */
     /** Authenticates the user using a named authentication strategy */
-    authenticate: LoginResult;
+    authenticate: AuthenticationResult;
     /** End the current authenticated session */
     /** End the current authenticated session */
-    logout: Scalars['Boolean'];
+    logout: Success;
     /**
     /**
      * Register a Customer account with the given credentials. There are three possible registration flows:
      * Register a Customer account with the given credentials. There are three possible registration flows:
      *
      *
@@ -1678,6 +1675,8 @@ export type MutationResetPasswordArgs = {
     password: Scalars['String'];
     password: Scalars['String'];
 };
 };
 
 
+export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type NativeAuthInput = {
 export type NativeAuthInput = {
     username: Scalars['String'];
     username: Scalars['String'];
     password: Scalars['String'];
     password: Scalars['String'];

+ 230 - 24
packages/common/src/generated-types.ts

@@ -16,6 +16,8 @@ export type Scalars = {
   Upload: any;
   Upload: any;
 };
 };
 
 
+export type AddFulfillmentToOrderResult = Fulfillment | EmptyOrderLineSelectionError | ItemsAlreadyFulfilledError;
+
 export type AddNoteToCustomerInput = {
 export type AddNoteToCustomerInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   note: Scalars['String'];
   note: Scalars['String'];
@@ -106,6 +108,14 @@ export type AdministratorSortParameter = {
   emailAddress?: Maybe<SortOrder>;
   emailAddress?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund an OrderItem which has already been refunded */
+export type AlreadyRefundedError = ErrorResult & {
+  __typename?: 'AlreadyRefundedError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  refundId: Scalars['ID'];
+};
+
 export type Asset = Node & {
 export type Asset = Node & {
   __typename?: 'Asset';
   __typename?: 'Asset';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -185,6 +195,8 @@ export type AuthenticationMethod = Node & {
   strategy: Scalars['String'];
   strategy: Scalars['String'];
 };
 };
 
 
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
   __typename?: 'BooleanCustomFieldConfig';
   __typename?: 'BooleanCustomFieldConfig';
   name: Scalars['String'];
   name: Scalars['String'];
@@ -200,6 +212,14 @@ export type BooleanOperators = {
   eq?: Maybe<Scalars['Boolean']>;
   eq?: Maybe<Scalars['Boolean']>;
 };
 };
 
 
+/** Returned if an attempting to cancel lines from an Order which is still active */
+export type CancelActiveOrderError = ErrorResult & {
+  __typename?: 'CancelActiveOrderError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  orderState: Scalars['String'];
+};
+
 export type Cancellation = Node & StockMovement & {
 export type Cancellation = Node & StockMovement & {
   __typename?: 'Cancellation';
   __typename?: 'Cancellation';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -219,6 +239,8 @@ export type CancelOrderInput = {
   reason?: Maybe<Scalars['String']>;
   reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type CancelOrderResult = Order | EmptyOrderLineSelectionError | QuantityTooGreatError | MultipleOrderError | CancelActiveOrderError | OrderStateTransitionError;
+
 export type Channel = Node & {
 export type Channel = Node & {
   __typename?: 'Channel';
   __typename?: 'Channel';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -233,6 +255,18 @@ export type Channel = Node & {
   pricesIncludeTax: Scalars['Boolean'];
   pricesIncludeTax: Scalars['Boolean'];
 };
 };
 
 
+/**
+ * Returned when the default LanguageCode of a Channel is no longer found in the `availableLanguages`
+ * of the GlobalSettings
+ */
+export type ChannelDefaultLanguageError = ErrorResult & {
+  __typename?: 'ChannelDefaultLanguageError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  language: Scalars['String'];
+  channelCode: Scalars['String'];
+};
+
 export type Collection = Node & {
 export type Collection = Node & {
   __typename?: 'Collection';
   __typename?: 'Collection';
   isPrivate: Scalars['Boolean'];
   isPrivate: Scalars['Boolean'];
@@ -458,6 +492,8 @@ export type CreateChannelInput = {
   defaultShippingZoneId: Scalars['ID'];
   defaultShippingZoneId: Scalars['ID'];
 };
 };
 
 
+export type CreateChannelResult = Channel | LanguageNotAvailableError;
+
 export type CreateCollectionInput = {
 export type CreateCollectionInput = {
   isPrivate?: Maybe<Scalars['Boolean']>;
   isPrivate?: Maybe<Scalars['Boolean']>;
   featuredAssetId?: Maybe<Scalars['ID']>;
   featuredAssetId?: Maybe<Scalars['ID']>;
@@ -496,6 +532,8 @@ export type CreateCustomerInput = {
   customFields?: Maybe<Scalars['JSON']>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type CreateCustomerResult = Customer | EmailAddressConflictError;
+
 export type CreateFacetInput = {
 export type CreateFacetInput = {
   code: Scalars['String'];
   code: Scalars['String'];
   isPrivate: Scalars['Boolean'];
   isPrivate: Scalars['Boolean'];
@@ -575,6 +613,8 @@ export type CreatePromotionInput = {
   actions: Array<ConfigurableOperationInput>;
   actions: Array<ConfigurableOperationInput>;
 };
 };
 
 
+export type CreatePromotionResult = Promotion | MissingConditionsError;
+
 export type CreateRoleInput = {
 export type CreateRoleInput = {
   code: Scalars['String'];
   code: Scalars['String'];
   description: Scalars['String'];
   description: Scalars['String'];
@@ -1122,10 +1162,44 @@ export enum DeletionResult {
   NOT_DELETED = 'NOT_DELETED'
   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'];
+};
+
+/** Returned if no OrderLines have been specified for the operation */
+export type EmptyOrderLineSelectionError = ErrorResult & {
+  __typename?: 'EmptyOrderLineSelectionError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export enum ErrorCode {
 export enum ErrorCode {
   UNKNOWN_ERROR = 'UNKNOWN_ERROR',
   UNKNOWN_ERROR = 'UNKNOWN_ERROR',
   MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
   MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
-  ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR'
+  INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
+  NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+  LANGUAGE_NOT_AVAILABLE_ERROR = 'LANGUAGE_NOT_AVAILABLE_ERROR',
+  EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+  CHANNEL_DEFAULT_LANGUAGE_ERROR = 'CHANNEL_DEFAULT_LANGUAGE_ERROR',
+  SETTLE_PAYMENT_ERROR = 'SETTLE_PAYMENT_ERROR',
+  PAYMENT_STATE_TRANSITION_ERROR = 'PAYMENT_STATE_TRANSITION_ERROR',
+  ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
+  EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
+  ITEMS_ALREADY_FULFILLED_ERROR = 'ITEMS_ALREADY_FULFILLED_ERROR',
+  QUANTITY_TOO_GREAT_ERROR = 'QUANTITY_TOO_GREAT_ERROR',
+  MULTIPLE_ORDER_ERROR = 'MULTIPLE_ORDER_ERROR',
+  CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
+  NOTHING_TO_REFUND_ERROR = 'NOTHING_TO_REFUND_ERROR',
+  PAYMENT_ORDER_MISMATCH_ERROR = 'PAYMENT_ORDER_MISMATCH_ERROR',
+  REFUND_ORDER_STATE_ERROR = 'REFUND_ORDER_STATE_ERROR',
+  ALREADY_REFUNDED_ERROR = 'ALREADY_REFUNDED_ERROR',
+  REFUND_STATE_TRANSITION_ERROR = 'REFUND_STATE_TRANSITION_ERROR',
+  FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+  PRODUCT_OPTION_IN_USE_ERROR = 'PRODUCT_OPTION_IN_USE_ERROR',
+  MISSING_CONDITIONS_ERROR = 'MISSING_CONDITIONS_ERROR'
 }
 }
 
 
 export type ErrorResult = {
 export type ErrorResult = {
@@ -1258,6 +1332,16 @@ export type Fulfillment = Node & {
   trackingCode?: Maybe<Scalars['String']>;
   trackingCode?: Maybe<Scalars['String']>;
 };
 };
 
 
+/** Returned when there is an error in transitioning the Fulfillment state */
+export type FulfillmentStateTransitionError = ErrorResult & {
+  __typename?: 'FulfillmentStateTransitionError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  transitionError: Scalars['String'];
+  fromState: Scalars['String'];
+  toState: Scalars['String'];
+};
+
 export type FulfillOrderInput = {
 export type FulfillOrderInput = {
   lines: Array<OrderLineInput>;
   lines: Array<OrderLineInput>;
   method: Scalars['String'];
   method: Scalars['String'];
@@ -1359,6 +1443,20 @@ export type IntCustomFieldConfig = CustomField & {
   step?: Maybe<Scalars['Int']>;
   step?: Maybe<Scalars['Int']>;
 };
 };
 
 
+/** Returned if the user authentication credentials are not valid */
+export type InvalidCredentialsError = ErrorResult & {
+  __typename?: 'InvalidCredentialsError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
+/** Returned if the specified items are already part of a Fulfillment */
+export type ItemsAlreadyFulfilledError = ErrorResult & {
+  __typename?: 'ItemsAlreadyFulfilledError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export type Job = Node & {
 export type Job = Node & {
   __typename?: 'Job';
   __typename?: 'Job';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -1756,6 +1854,13 @@ export enum LanguageCode {
   zu = 'zu'
   zu = 'zu'
 }
 }
 
 
+export type LanguageNotAvailableError = ErrorResult & {
+  __typename?: 'LanguageNotAvailableError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  languageCode: Scalars['String'];
+};
+
 export type LocaleStringCustomFieldConfig = CustomField & {
 export type LocaleStringCustomFieldConfig = CustomField & {
   __typename?: 'LocaleStringCustomFieldConfig';
   __typename?: 'LocaleStringCustomFieldConfig';
   name: Scalars['String'];
   name: Scalars['String'];
@@ -1780,11 +1885,6 @@ export enum LogicalOperator {
   OR = 'OR'
   OR = 'OR'
 }
 }
 
 
-export type LoginResult = {
-  __typename?: 'LoginResult';
-  user: CurrentUser;
-};
-
 export type MimeTypeError = ErrorResult & {
 export type MimeTypeError = ErrorResult & {
   __typename?: 'MimeTypeError';
   __typename?: 'MimeTypeError';
   code: ErrorCode;
   code: ErrorCode;
@@ -1793,12 +1893,26 @@ export type MimeTypeError = ErrorResult & {
   mimeType: Scalars['String'];
   mimeType: Scalars['String'];
 };
 };
 
 
+/** Returned if a PromotionCondition has neither a couponCode nor any conditions set */
+export type MissingConditionsError = ErrorResult & {
+  __typename?: 'MissingConditionsError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export type MoveCollectionInput = {
 export type MoveCollectionInput = {
   collectionId: Scalars['ID'];
   collectionId: Scalars['ID'];
   parentId: Scalars['ID'];
   parentId: Scalars['ID'];
   index: Scalars['Int'];
   index: Scalars['Int'];
 };
 };
 
 
+/** Returned if an operation has specified OrderLines from multiple Orders */
+export type MultipleOrderError = ErrorResult & {
+  __typename?: 'MultipleOrderError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export type Mutation = {
 export type Mutation = {
   __typename?: 'Mutation';
   __typename?: 'Mutation';
   /** Create a new Administrator */
   /** Create a new Administrator */
@@ -1821,14 +1935,14 @@ export type Mutation = {
    * Authenticates the user using the native authentication strategy. This mutation
    * Authenticates the user using the native authentication strategy. This mutation
    * is an alias for `authenticate({ native: { ... }})`
    * is an alias for `authenticate({ native: { ... }})`
    */
    */
-  login: LoginResult;
+  login: NativeAuthenticationResult;
   /** Authenticates the user using a named authentication strategy */
   /** Authenticates the user using a named authentication strategy */
-  authenticate: LoginResult;
-  logout: Scalars['Boolean'];
+  authenticate: AuthenticationResult;
+  logout: Success;
   /** Create a new Channel */
   /** Create a new Channel */
-  createChannel: Channel;
+  createChannel: CreateChannelResult;
   /** Update an existing Channel */
   /** Update an existing Channel */
-  updateChannel: Channel;
+  updateChannel: UpdateChannelResult;
   /** Delete a Channel */
   /** Delete a Channel */
   deleteChannel: DeletionResponse;
   deleteChannel: DeletionResponse;
   /** Create a new Collection */
   /** Create a new Collection */
@@ -1856,9 +1970,9 @@ export type Mutation = {
   /** Remove Customers from a CustomerGroup */
   /** Remove Customers from a CustomerGroup */
   removeCustomersFromGroup: CustomerGroup;
   removeCustomersFromGroup: CustomerGroup;
   /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
   /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
-  createCustomer: Customer;
+  createCustomer: CreateCustomerResult;
   /** Update an existing Customer */
   /** Update an existing Customer */
-  updateCustomer: Customer;
+  updateCustomer: UpdateCustomerResult;
   /** Delete a Customer */
   /** Delete a Customer */
   deleteCustomer: DeletionResponse;
   deleteCustomer: DeletionResponse;
   /** Create a new Address and associate it with the Customer specified by customerId */
   /** Create a new Address and associate it with the Customer specified by customerId */
@@ -1882,20 +1996,20 @@ export type Mutation = {
   updateFacetValues: Array<FacetValue>;
   updateFacetValues: Array<FacetValue>;
   /** Delete one or more FacetValues */
   /** Delete one or more FacetValues */
   deleteFacetValues: Array<DeletionResponse>;
   deleteFacetValues: Array<DeletionResponse>;
-  updateGlobalSettings: GlobalSettings;
+  updateGlobalSettings: UpdateGlobalSettingsResult;
   importProducts?: Maybe<ImportInfo>;
   importProducts?: Maybe<ImportInfo>;
   /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
   /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
   removeSettledJobs: Scalars['Int'];
   removeSettledJobs: Scalars['Int'];
-  settlePayment: Payment;
-  fulfillOrder: Fulfillment;
-  cancelOrder: Order;
-  refundOrder: Refund;
-  settleRefund: Refund;
+  settlePayment: SettlePaymentResult;
+  addFulfillmentToOrder: AddFulfillmentToOrderResult;
+  cancelOrder: CancelOrderResult;
+  refundOrder: RefundOrderResult;
+  settleRefund: SettleRefundResult;
   addNoteToOrder: Order;
   addNoteToOrder: Order;
   updateOrderNote: HistoryEntry;
   updateOrderNote: HistoryEntry;
   deleteOrderNote: DeletionResponse;
   deleteOrderNote: DeletionResponse;
   transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
   transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
-  transitionFulfillmentToState: Fulfillment;
+  transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
   /** Update an existing PaymentMethod */
   /** Update an existing PaymentMethod */
   updatePaymentMethod: PaymentMethod;
   updatePaymentMethod: PaymentMethod;
@@ -1917,7 +2031,7 @@ export type Mutation = {
   /** Add an OptionGroup to a Product */
   /** Add an OptionGroup to a Product */
   addOptionGroupToProduct: Product;
   addOptionGroupToProduct: Product;
   /** Remove an OptionGroup from a Product */
   /** Remove an OptionGroup from a Product */
-  removeOptionGroupFromProduct: Product;
+  removeOptionGroupFromProduct: RemoveOptionGroupFromProductResult;
   /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
   /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
   createProductVariants: Array<Maybe<ProductVariant>>;
   createProductVariants: Array<Maybe<ProductVariant>>;
   /** Update existing ProductVariants */
   /** Update existing ProductVariants */
@@ -1928,8 +2042,8 @@ export type Mutation = {
   assignProductsToChannel: Array<Product>;
   assignProductsToChannel: Array<Product>;
   /** Removes Products from the specified Channel */
   /** Removes Products from the specified Channel */
   removeProductsFromChannel: Array<Product>;
   removeProductsFromChannel: Array<Product>;
-  createPromotion: Promotion;
-  updatePromotion: Promotion;
+  createPromotion: CreatePromotionResult;
+  updatePromotion: UpdatePromotionResult;
   deletePromotion: DeletionResponse;
   deletePromotion: DeletionResponse;
   /** Create a new Role */
   /** Create a new Role */
   createRole: Role;
   createRole: Role;
@@ -2201,7 +2315,7 @@ export type MutationSettlePaymentArgs = {
 };
 };
 
 
 
 
-export type MutationFulfillOrderArgs = {
+export type MutationAddFulfillmentToOrderArgs = {
   input: FulfillOrderInput;
   input: FulfillOrderInput;
 };
 };
 
 
@@ -2431,15 +2545,31 @@ export type MutationRemoveMembersFromZoneArgs = {
   memberIds: Array<Scalars['ID']>;
   memberIds: Array<Scalars['ID']>;
 };
 };
 
 
+export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type NativeAuthInput = {
 export type NativeAuthInput = {
   username: Scalars['String'];
   username: Scalars['String'];
   password: Scalars['String'];
   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'];
+};
+
 export type Node = {
 export type Node = {
   id: Scalars['ID'];
   id: Scalars['ID'];
 };
 };
 
 
+/** Returned if an attempting to refund an Order but neither items nor shipping refund was specified */
+export type NothingToRefundError = ErrorResult & {
+  __typename?: 'NothingToRefundError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export type NumberOperators = {
 export type NumberOperators = {
   eq?: Maybe<Scalars['Float']>;
   eq?: Maybe<Scalars['Float']>;
   lt?: Maybe<Scalars['Float']>;
   lt?: Maybe<Scalars['Float']>;
@@ -2660,6 +2790,23 @@ export type PaymentMethodSortParameter = {
   code?: Maybe<SortOrder>;
   code?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund a Payment against OrderLines from a different Order */
+export type PaymentOrderMismatchError = ErrorResult & {
+  __typename?: 'PaymentOrderMismatchError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Payment state */
+export type PaymentStateTransitionError = ErrorResult & {
+  __typename?: 'PaymentStateTransitionError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  transitionError: Scalars['String'];
+  fromState: Scalars['String'];
+  toState: Scalars['String'];
+};
+
 /**
 /**
  * "
  * "
  * @description
  * @description
@@ -2797,6 +2944,14 @@ export type ProductOptionGroupTranslationInput = {
   customFields?: Maybe<Scalars['JSON']>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type ProductOptionInUseError = ErrorResult & {
+  __typename?: 'ProductOptionInUseError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  optionGroupCode: Scalars['String'];
+  productVariantCount: Scalars['Int'];
+};
+
 export type ProductOptionTranslation = {
 export type ProductOptionTranslation = {
   __typename?: 'ProductOptionTranslation';
   __typename?: 'ProductOptionTranslation';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -2981,6 +3136,13 @@ export type PromotionSortParameter = {
   name?: Maybe<SortOrder>;
   name?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
+export type QuantityTooGreatError = ErrorResult & {
+  __typename?: 'QuantityTooGreatError';
+  code: ErrorCode;
+  message: Scalars['String'];
+};
+
 export type Query = {
 export type Query = {
   __typename?: 'Query';
   __typename?: 'Query';
   administrators: AdministratorList;
   administrators: AdministratorList;
@@ -3270,6 +3432,28 @@ export type RefundOrderInput = {
   reason?: Maybe<Scalars['String']>;
   reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type RefundOrderResult = Refund | QuantityTooGreatError | NothingToRefundError | OrderStateTransitionError | MultipleOrderError | PaymentOrderMismatchError | RefundOrderStateError | AlreadyRefundedError | RefundStateTransitionError;
+
+/** Returned if an attempting to refund an Order which is not in the expected state */
+export type RefundOrderStateError = ErrorResult & {
+  __typename?: 'RefundOrderStateError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  orderState: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Refund state */
+export type RefundStateTransitionError = ErrorResult & {
+  __typename?: 'RefundStateTransitionError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  transitionError: Scalars['String'];
+  fromState: Scalars['String'];
+  toState: Scalars['String'];
+};
+
+export type RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError;
+
 export type RemoveProductsFromChannelInput = {
 export type RemoveProductsFromChannelInput = {
   productIds: Array<Scalars['ID']>;
   productIds: Array<Scalars['ID']>;
   channelId: Scalars['ID'];
   channelId: Scalars['ID'];
@@ -3411,11 +3595,23 @@ export type ServerConfig = {
   customFieldConfig: CustomFields;
   customFieldConfig: CustomFields;
 };
 };
 
 
+/** Returned if the Payment settlement fails */
+export type SettlePaymentError = ErrorResult & {
+  __typename?: 'SettlePaymentError';
+  code: ErrorCode;
+  message: Scalars['String'];
+  paymentErrorMessage: Scalars['String'];
+};
+
+export type SettlePaymentResult = Payment | SettlePaymentError | PaymentStateTransitionError | OrderStateTransitionError;
+
 export type SettleRefundInput = {
 export type SettleRefundInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   transactionId: Scalars['String'];
   transactionId: Scalars['String'];
 };
 };
 
 
+export type SettleRefundResult = Refund | RefundStateTransitionError;
+
 export type ShippingMethod = Node & {
 export type ShippingMethod = Node & {
   __typename?: 'ShippingMethod';
   __typename?: 'ShippingMethod';
   id: Scalars['ID'];
   id: Scalars['ID'];
@@ -3628,6 +3824,8 @@ export type TestShippingMethodResult = {
   quote?: Maybe<TestShippingMethodQuote>;
   quote?: Maybe<TestShippingMethodQuote>;
 };
 };
 
 
+export type TransitionFulfillmentToStateResult = Fulfillment | FulfillmentStateTransitionError;
+
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 
 
 export type UpdateAddressInput = {
 export type UpdateAddressInput = {
@@ -3672,6 +3870,8 @@ export type UpdateChannelInput = {
   defaultShippingZoneId?: Maybe<Scalars['ID']>;
   defaultShippingZoneId?: Maybe<Scalars['ID']>;
 };
 };
 
 
+export type UpdateChannelResult = Channel | LanguageNotAvailableError;
+
 export type UpdateCollectionInput = {
 export type UpdateCollectionInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   isPrivate?: Maybe<Scalars['Boolean']>;
   isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3719,6 +3919,8 @@ export type UpdateCustomerNoteInput = {
   note: Scalars['String'];
   note: Scalars['String'];
 };
 };
 
 
+export type UpdateCustomerResult = Customer | EmailAddressConflictError;
+
 export type UpdateFacetInput = {
 export type UpdateFacetInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   isPrivate?: Maybe<Scalars['Boolean']>;
   isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3740,6 +3942,8 @@ export type UpdateGlobalSettingsInput = {
   customFields?: Maybe<Scalars['JSON']>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type UpdateGlobalSettingsResult = GlobalSettings | ChannelDefaultLanguageError;
+
 export type UpdateOrderInput = {
 export type UpdateOrderInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   customFields?: Maybe<Scalars['JSON']>;
   customFields?: Maybe<Scalars['JSON']>;
@@ -3809,6 +4013,8 @@ export type UpdatePromotionInput = {
   actions?: Maybe<Array<ConfigurableOperationInput>>;
   actions?: Maybe<Array<ConfigurableOperationInput>>;
 };
 };
 
 
+export type UpdatePromotionResult = Promotion | MissingConditionsError;
+
 export type UpdateRoleInput = {
 export type UpdateRoleInput = {
   id: Scalars['ID'];
   id: Scalars['ID'];
   code?: Maybe<Scalars['String']>;
   code?: Maybe<Scalars['String']>;

+ 10 - 10
packages/core/e2e/auth.e2e-spec.ts

@@ -11,6 +11,7 @@ import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-conf
 import {
 import {
     CreateAdministrator,
     CreateAdministrator,
     CreateRole,
     CreateRole,
+    ErrorCode,
     GetCustomerList,
     GetCustomerList,
     Me,
     Me,
     MutationCreateProductArgs,
     MutationCreateProductArgs,
@@ -76,12 +77,11 @@ describe('Authorization & permissions', () => {
                 customerEmailAddress = customers.items[0].emailAddress;
                 customerEmailAddress = customers.items[0].emailAddress;
             });
             });
 
 
-            it(
-                'cannot login',
-                assertThrowsWithMessage(async () => {
-                    await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
-                }, 'The credentials did not match. Please check and try again'),
-            );
+            it('cannot login', async () => {
+                const result = await adminClient.asUserWithCredentials(customerEmailAddress, 'test');
+
+                expect(result.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
+            });
         });
         });
 
 
         describe('ReadCatalog permission', () => {
         describe('ReadCatalog permission', () => {
@@ -153,7 +153,9 @@ describe('Authorization & permissions', () => {
                     gql`
                     gql`
                         mutation CanCreateCustomer($input: CreateCustomerInput!) {
                         mutation CanCreateCustomer($input: CreateCustomerInput!) {
                             createCustomer(input: $input) {
                             createCustomer(input: $input) {
-                                id
+                                ... on Customer {
+                                    id
+                                }
                             }
                             }
                         }
                         }
                     `,
                     `,
@@ -214,9 +216,7 @@ describe('Authorization & permissions', () => {
 
 
         const role = roleResult.createRole;
         const role = roleResult.createRole;
 
 
-        const identifier = `${code}@${Math.random()
-            .toString(16)
-            .substr(2, 8)}`;
+        const identifier = `${code}@${Math.random().toString(16).substr(2, 8)}`;
         const password = `test`;
         const password = `test`;
 
 
         const adminResult = await adminClient.query<
         const adminResult = await adminClient.query<

+ 32 - 18
packages/core/e2e/authentication-strategy.e2e-spec.ts

@@ -1,3 +1,4 @@
+import { ErrorCode } from '@vendure/common/lib/generated-shop-types';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { mergeConfig } from '@vendure/core';
 import { mergeConfig } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
@@ -10,8 +11,11 @@ import { NativeAuthenticationStrategy } from '../src/config/auth/native-authenti
 import { DefaultLogger } from '../src/config/logger/default-logger';
 import { DefaultLogger } from '../src/config/logger/default-logger';
 
 
 import { TestAuthenticationStrategy, VALID_AUTH_TOKEN } from './fixtures/test-authentication-strategies';
 import { TestAuthenticationStrategy, VALID_AUTH_TOKEN } from './fixtures/test-authentication-strategies';
+import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
 import {
 import {
     Authenticate,
     Authenticate,
+    CurrentUser,
+    CurrentUserFragment,
     GetCustomerHistory,
     GetCustomerHistory,
     GetCustomers,
     GetCustomers,
     GetCustomerUserAuth,
     GetCustomerUserAuth,
@@ -48,6 +52,10 @@ describe('AuthenticationStrategy', () => {
         await server.destroy();
         await server.destroy();
     });
     });
 
 
+    const currentUserGuard: ErrorResultGuard<CurrentUserFragment> = createErrorResultGuard<
+        CurrentUserFragment
+    >(input => input.identifier != null);
+
     describe('external auth', () => {
     describe('external auth', () => {
         const userData = {
         const userData = {
             email: 'test@email.com',
             email: 'test@email.com',
@@ -56,24 +64,24 @@ describe('AuthenticationStrategy', () => {
         };
         };
         let newCustomerId: string;
         let newCustomerId: string;
 
 
-        it(
-            'fails with a bad token',
-            assertThrowsWithMessage(async () => {
-                await shopClient.query(AUTHENTICATE, {
-                    input: {
-                        test_strategy: {
-                            token: 'bad-token',
-                        },
+        it('fails with a bad token', async () => {
+            const { authenticate } = await shopClient.query(AUTHENTICATE, {
+                input: {
+                    test_strategy: {
+                        token: 'bad-token',
                     },
                     },
-                });
-            }, 'The credentials did not match. Please check and try again'),
-        );
+                },
+            });
+
+            expect(authenticate.message).toBe('The provided credentials are invalid');
+            expect(authenticate.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
+        });
 
 
         it('creates a new Customer with valid token', async () => {
         it('creates a new Customer with valid token', async () => {
             const { customers: before } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: before } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(before.totalItems).toBe(1);
             expect(before.totalItems).toBe(1);
 
 
-            const result = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
+            const { authenticate } = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
                 input: {
                 input: {
                     test_strategy: {
                     test_strategy: {
                         token: VALID_AUTH_TOKEN,
                         token: VALID_AUTH_TOKEN,
@@ -81,7 +89,9 @@ describe('AuthenticationStrategy', () => {
                     },
                     },
                 },
                 },
             });
             });
-            expect(result.authenticate.user.identifier).toEqual(userData.email);
+            currentUserGuard.assertSuccess(authenticate);
+
+            expect(authenticate.identifier).toEqual(userData.email);
 
 
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(after.totalItems).toBe(2);
             expect(after.totalItems).toBe(2);
@@ -141,7 +151,7 @@ describe('AuthenticationStrategy', () => {
         });
         });
 
 
         it('logging in again re-uses created User & Customer', async () => {
         it('logging in again re-uses created User & Customer', async () => {
-            const result = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
+            const { authenticate } = await shopClient.query<Authenticate.Mutation>(AUTHENTICATE, {
                 input: {
                 input: {
                     test_strategy: {
                     test_strategy: {
                         token: VALID_AUTH_TOKEN,
                         token: VALID_AUTH_TOKEN,
@@ -149,7 +159,9 @@ describe('AuthenticationStrategy', () => {
                     },
                     },
                 },
                 },
             });
             });
-            expect(result.authenticate.user.identifier).toEqual(userData.email);
+            currentUserGuard.assertSuccess(authenticate);
+
+            expect(authenticate.identifier).toEqual(userData.email);
 
 
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             const { customers: after } = await adminClient.query<GetCustomers.Query>(GET_CUSTOMERS);
             expect(after.totalItems).toBe(2);
             expect(after.totalItems).toBe(2);
@@ -210,12 +222,14 @@ describe('AuthenticationStrategy', () => {
 const AUTHENTICATE = gql`
 const AUTHENTICATE = gql`
     mutation Authenticate($input: AuthenticationInput!) {
     mutation Authenticate($input: AuthenticationInput!) {
         authenticate(input: $input) {
         authenticate(input: $input) {
-            user {
-                id
-                identifier
+            ...CurrentUser
+            ... on ErrorResult {
+                code
+                message
             }
             }
         }
         }
     }
     }
+    ${CURRENT_USER_FRAGMENT}
 `;
 `;
 
 
 const GET_CUSTOMERS = gql`
 const GET_CUSTOMERS = gql`

+ 65 - 34
packages/core/e2e/channel.e2e-spec.ts

@@ -1,20 +1,27 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
-import { createTestEnvironment, E2E_DEFAULT_CHANNEL_TOKEN } from '@vendure/testing';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    E2E_DEFAULT_CHANNEL_TOKEN,
+    ErrorResultGuard,
+} from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 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 {
 import {
     AssignProductsToChannel,
     AssignProductsToChannel,
+    ChannelFragment,
     CreateAdministrator,
     CreateAdministrator,
     CreateChannel,
     CreateChannel,
     CreateRole,
     CreateRole,
     CurrencyCode,
     CurrencyCode,
     DeleteChannel,
     DeleteChannel,
     DeletionResult,
     DeletionResult,
+    ErrorCode,
     GetChannels,
     GetChannels,
     GetCustomerList,
     GetCustomerList,
     GetProductWithVariants,
     GetProductWithVariants,
@@ -23,7 +30,7 @@ import {
     Permission,
     Permission,
     RemoveProductsFromChannel,
     RemoveProductsFromChannel,
     UpdateChannel,
     UpdateChannel,
-    UpdateGlobalSettings,
+    UpdateGlobalLanguages,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
     ASSIGN_PRODUCT_TO_CHANNEL,
     ASSIGN_PRODUCT_TO_CHANNEL,
@@ -45,6 +52,10 @@ describe('Channels', () => {
     let secondChannelAdminRole: CreateRole.CreateRole;
     let secondChannelAdminRole: CreateRole.CreateRole;
     let customerUser: GetCustomerList.Items;
     let customerUser: GetCustomerList.Items;
 
 
+    const channelGuard: ErrorResultGuard<ChannelFragment> = createErrorResultGuard<ChannelFragment>(
+        input => !!input.defaultLanguageCode,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -66,6 +77,30 @@ describe('Channels', () => {
         await server.destroy();
         await server.destroy();
     });
     });
 
 
+    it('createChannel returns error result defaultLanguageCode not available', async () => {
+        const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
+            CREATE_CHANNEL,
+            {
+                input: {
+                    code: 'second-channel',
+                    token: SECOND_CHANNEL_TOKEN,
+                    defaultLanguageCode: LanguageCode.zh,
+                    currencyCode: CurrencyCode.GBP,
+                    pricesIncludeTax: true,
+                    defaultShippingZoneId: 'T_1',
+                    defaultTaxZoneId: 'T_1',
+                },
+            },
+        );
+        channelGuard.assertErrorResult(createChannel);
+
+        expect(createChannel.message).toBe(
+            'Language "zh" is not available. First enable it via GlobalSettings and try again',
+        );
+        expect(createChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
+        expect(createChannel.languageCode).toBe('zh');
+    });
+
     it('create a new Channel', async () => {
     it('create a new Channel', async () => {
         const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
         const { createChannel } = await adminClient.query<CreateChannel.Mutation, CreateChannel.Variables>(
             CREATE_CHANNEL,
             CREATE_CHANNEL,
@@ -81,6 +116,7 @@ describe('Channels', () => {
                 },
                 },
             },
             },
         );
         );
+        channelGuard.assertSuccess(createChannel);
 
 
         expect(createChannel).toEqual({
         expect(createChannel).toEqual({
             id: 'T_2',
             id: 'T_2',
@@ -356,21 +392,28 @@ describe('Channels', () => {
     });
     });
 
 
     describe('setting defaultLanguage', () => {
     describe('setting defaultLanguage', () => {
-        it(
-            'throws if languageCode not in availableLanguages',
-            assertThrowsWithMessage(async () => {
-                await adminClient.query<UpdateChannel.Mutation, UpdateChannel.Variables>(UPDATE_CHANNEL, {
-                    input: {
-                        id: 'T_1',
-                        defaultLanguageCode: LanguageCode.zh,
-                    },
-                });
-            }, 'Language "zh" is not available. First enable it via GlobalSettings and try again.'),
-        );
+        it('returns error result if languageCode not in availableLanguages', async () => {
+            const { updateChannel } = await adminClient.query<
+                UpdateChannel.Mutation,
+                UpdateChannel.Variables
+            >(UPDATE_CHANNEL, {
+                input: {
+                    id: 'T_1',
+                    defaultLanguageCode: LanguageCode.zh,
+                },
+            });
+            channelGuard.assertErrorResult(updateChannel);
+
+            expect(updateChannel.message).toBe(
+                'Language "zh" is not available. First enable it via GlobalSettings and try again',
+            );
+            expect(updateChannel.errorCode).toBe(ErrorCode.LANGUAGE_NOT_AVAILABLE_ERROR);
+            expect(updateChannel.languageCode).toBe('zh');
+        });
 
 
         it('allows setting to an available language', async () => {
         it('allows setting to an available language', async () => {
-            await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
-                UPDATE_GLOBAL_SETTINGS,
+            await adminClient.query<UpdateGlobalLanguages.Mutation, UpdateGlobalLanguages.Variables>(
+                UPDATE_GLOBAL_LANGUAGES,
                 {
                 {
                     input: {
                     input: {
                         availableLanguages: [LanguageCode.en, LanguageCode.zh],
                         availableLanguages: [LanguageCode.en, LanguageCode.zh],
@@ -390,20 +433,6 @@ describe('Channels', () => {
 
 
             expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
             expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.zh);
         });
         });
-
-        it(
-            'attempting to remove availableLanguage when used by a Channel throws',
-            assertThrowsWithMessage(async () => {
-                await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
-                    UPDATE_GLOBAL_SETTINGS,
-                    {
-                        input: {
-                            availableLanguages: [LanguageCode.en],
-                        },
-                    },
-                );
-            }, 'Cannot remove make language "zh" unavailable as it is used as the defaultLanguage by the channel "__default_channel__"'),
-        );
     });
     });
 
 
     it('deleteChannel', async () => {
     it('deleteChannel', async () => {
@@ -461,11 +490,13 @@ const DELETE_CHANNEL = gql`
     }
     }
 `;
 `;
 
 
-const UPDATE_GLOBAL_SETTINGS = gql`
-    mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
+const UPDATE_GLOBAL_LANGUAGES = gql`
+    mutation UpdateGlobalLanguages($input: UpdateGlobalSettingsInput!) {
         updateGlobalSettings(input: $input) {
         updateGlobalSettings(input: $input) {
-            id
-            availableLanguages
+            ... on GlobalSettings {
+                id
+                availableLanguages
+            }
         }
         }
     }
     }
 `;
 `;

+ 3 - 1
packages/core/e2e/custom-fields.e2e-spec.ts

@@ -327,7 +327,9 @@ describe('Custom fields', () => {
             await adminClient.query(gql`
             await adminClient.query(gql`
                 mutation {
                 mutation {
                     updateCustomer(input: { id: "T_1", customFields: { score: 5 } }) {
                     updateCustomer(input: { id: "T_1", customFields: { score: 5 } }) {
-                        id
+                        ... on Customer {
+                            id
+                        }
                     }
                     }
                 }
                 }
             `);
             `);

+ 59 - 16
packages/core/e2e/customer.e2e-spec.ts

@@ -21,10 +21,12 @@ import {
     AddNoteToCustomer,
     AddNoteToCustomer,
     CreateAddress,
     CreateAddress,
     CreateCustomer,
     CreateCustomer,
+    CustomerFragment,
     DeleteCustomer,
     DeleteCustomer,
     DeleteCustomerAddress,
     DeleteCustomerAddress,
     DeleteCustomerNote,
     DeleteCustomerNote,
     DeletionResult,
     DeletionResult,
+    ErrorCode,
     GetCustomer,
     GetCustomer,
     GetCustomerHistory,
     GetCustomerHistory,
     GetCustomerList,
     GetCustomerList,
@@ -78,6 +80,10 @@ describe('Customer resolver', () => {
     let secondCustomer: GetCustomerList.Items;
     let secondCustomer: GetCustomerList.Items;
     let thirdCustomer: GetCustomerList.Items;
     let thirdCustomer: GetCustomerList.Items;
 
 
+    const customerErrorGuard: ErrorResultGuard<CustomerFragment> = createErrorResultGuard<CustomerFragment>(
+        input => !!input.emailAddress,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -400,6 +406,7 @@ describe('Customer resolver', () => {
                     lastName: 'Customer',
                     lastName: 'Customer',
                 },
                 },
             });
             });
+            customerErrorGuard.assertSuccess(createCustomer);
 
 
             expect(createCustomer.user!.verified).toBe(false);
             expect(createCustomer.user!.verified).toBe(false);
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
             expect(sendEmailFn).toHaveBeenCalledTimes(1);
@@ -420,27 +427,62 @@ describe('Customer resolver', () => {
                 },
                 },
                 password: 'test',
                 password: 'test',
             });
             });
+            customerErrorGuard.assertSuccess(createCustomer);
 
 
             expect(createCustomer.user!.verified).toBe(true);
             expect(createCustomer.user!.verified).toBe(true);
             expect(sendEmailFn).toHaveBeenCalledTimes(0);
             expect(sendEmailFn).toHaveBeenCalledTimes(0);
         });
         });
 
 
-        it(
-            'throws when using an existing, non-deleted emailAddress',
-            assertThrowsWithMessage(async () => {
-                const { createCustomer } = await adminClient.query<
-                    CreateCustomer.Mutation,
-                    CreateCustomer.Variables
-                >(CREATE_CUSTOMER, {
-                    input: {
-                        emailAddress: 'test2@test.com',
-                        firstName: 'New',
-                        lastName: 'Customer',
-                    },
-                    password: 'test',
-                });
-            }, 'The email address must be unique'),
-        );
+        it('return error result when using an existing, non-deleted emailAddress', async () => {
+            const { createCustomer } = await adminClient.query<
+                CreateCustomer.Mutation,
+                CreateCustomer.Variables
+            >(CREATE_CUSTOMER, {
+                input: {
+                    emailAddress: 'test2@test.com',
+                    firstName: 'New',
+                    lastName: 'Customer',
+                },
+                password: 'test',
+            });
+            customerErrorGuard.assertErrorResult(createCustomer);
+
+            expect(createCustomer.message).toBe('The email address is not available.');
+            expect(createCustomer.code).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
+        });
+    });
+
+    describe('update', () => {
+        it('returns error result when emailAddress not available', async () => {
+            const { updateCustomer } = await adminClient.query<
+                UpdateCustomer.Mutation,
+                UpdateCustomer.Variables
+            >(UPDATE_CUSTOMER, {
+                input: {
+                    id: thirdCustomer.id,
+                    emailAddress: firstCustomer.emailAddress,
+                },
+            });
+            customerErrorGuard.assertErrorResult(updateCustomer);
+
+            expect(updateCustomer.message).toBe('The email address is not available.');
+            expect(updateCustomer.code).toBe(ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR);
+        });
+
+        it('succeeds when emailAddress is available', async () => {
+            const { updateCustomer } = await adminClient.query<
+                UpdateCustomer.Mutation,
+                UpdateCustomer.Variables
+            >(UPDATE_CUSTOMER, {
+                input: {
+                    id: thirdCustomer.id,
+                    emailAddress: 'unique-email@test.com',
+                },
+            });
+            customerErrorGuard.assertSuccess(updateCustomer);
+
+            expect(updateCustomer.emailAddress).toBe('unique-email@test.com');
+        });
     });
     });
 
 
     describe('deletion', () => {
     describe('deletion', () => {
@@ -509,6 +551,7 @@ describe('Customer resolver', () => {
                     lastName: 'Customer',
                     lastName: 'Customer',
                 },
                 },
             });
             });
+            customerErrorGuard.assertSuccess(createCustomer);
 
 
             expect(createCustomer.emailAddress).toBe(thirdCustomer.emailAddress);
             expect(createCustomer.emailAddress).toBe(thirdCustomer.emailAddress);
             expect(createCustomer.firstName).toBe('Reusing Email');
             expect(createCustomer.firstName).toBe('Reusing Email');

+ 149 - 0
packages/core/e2e/global-settings.e2e-spec.ts

@@ -0,0 +1,149 @@
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+
+import {
+    GetGlobalSettings,
+    GlobalSettingsFragment,
+    LanguageCode,
+    UpdateGlobalSettings,
+} from './graphql/generated-e2e-admin-types';
+
+describe('GlobalSettings resolver', () => {
+    const { server, adminClient } = createTestEnvironment({
+        ...testConfig,
+        ...{
+            customFields: {
+                Customer: [{ name: 'age', type: 'int' }],
+            },
+        },
+    });
+    let globalSettings: GlobalSettingsFragment;
+
+    const globalSettingsGuard: ErrorResultGuard<GlobalSettingsFragment> = createErrorResultGuard<
+        GlobalSettingsFragment
+    >(input => !!input.availableLanguages);
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+        const result = await adminClient.query<GetGlobalSettings.Query>(GET_GLOBAL_SETTINGS);
+        globalSettings = result.globalSettings;
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('globalSettings query', () => {
+        it('includes basic settings', () => {
+            expect(globalSettings.availableLanguages).toEqual([LanguageCode.en]);
+            expect(globalSettings.trackInventory).toBe(false);
+        });
+
+        it('includes orderProcess', () => {
+            expect(globalSettings.serverConfig.orderProcess[0]).toEqual({
+                name: 'AddingItems',
+                to: ['ArrangingPayment', 'Cancelled'],
+            });
+        });
+
+        it('includes permittedAssetTypes', () => {
+            expect(globalSettings.serverConfig.permittedAssetTypes).toEqual([
+                'image/*',
+                'video/*',
+                'audio/*',
+                '.pdf',
+            ]);
+        });
+
+        it('includes customFieldConfig', () => {
+            expect(globalSettings.serverConfig.customFieldConfig.Customer).toEqual([{ name: 'age' }]);
+        });
+    });
+
+    describe('update', () => {
+        it('returns error result when removing required language', async () => {
+            const { updateGlobalSettings } = await adminClient.query<
+                UpdateGlobalSettings.Mutation,
+                UpdateGlobalSettings.Variables
+            >(UPDATE_GLOBAL_SETTINGS, {
+                input: {
+                    availableLanguages: [LanguageCode.zh],
+                },
+            });
+            globalSettingsGuard.assertErrorResult(updateGlobalSettings);
+
+            expect(updateGlobalSettings.message).toBe(
+                'Cannot make language "en" unavailable as it is used as the defaultLanguage by the channel "__default_channel__"',
+            );
+        });
+
+        it('successful update', async () => {
+            const { updateGlobalSettings } = await adminClient.query<
+                UpdateGlobalSettings.Mutation,
+                UpdateGlobalSettings.Variables
+            >(UPDATE_GLOBAL_SETTINGS, {
+                input: {
+                    availableLanguages: [LanguageCode.en, LanguageCode.zh],
+                    trackInventory: true,
+                },
+            });
+            globalSettingsGuard.assertSuccess(updateGlobalSettings);
+
+            expect(updateGlobalSettings.availableLanguages).toEqual([LanguageCode.en, LanguageCode.zh]);
+            expect(updateGlobalSettings.trackInventory).toBe(true);
+        });
+    });
+});
+
+const GLOBAL_SETTINGS_FRAGMENT = gql`
+    fragment GlobalSettings on GlobalSettings {
+        id
+        availableLanguages
+        trackInventory
+        serverConfig {
+            orderProcess {
+                name
+                to
+            }
+            permittedAssetTypes
+            customFieldConfig {
+                Customer {
+                    ... on CustomField {
+                        name
+                    }
+                }
+            }
+        }
+    }
+`;
+
+const GET_GLOBAL_SETTINGS = gql`
+    query GetGlobalSettings {
+        globalSettings {
+            ...GlobalSettings
+        }
+    }
+    ${GLOBAL_SETTINGS_FRAGMENT}
+`;
+
+const UPDATE_GLOBAL_SETTINGS = gql`
+    mutation UpdateGlobalSettings($input: UpdateGlobalSettingsInput!) {
+        updateGlobalSettings(input: $input) {
+            ...GlobalSettings
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
+    }
+    ${GLOBAL_SETTINGS_FRAGMENT}
+`;

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

@@ -481,3 +481,33 @@ export const VARIANT_WITH_STOCK_FRAGMENT = gql`
         }
         }
     }
     }
 `;
 `;
+
+export const FULFILLMENT_FRAGMENT = gql`
+    fragment Fulfillment on Fulfillment {
+        id
+        state
+        nextStates
+        method
+        trackingCode
+        orderItems {
+            id
+        }
+    }
+`;
+
+export const CHANNEL_FRAGMENT = gql`
+    fragment Channel on Channel {
+        id
+        code
+        token
+        currencyCode
+        defaultLanguageCode
+        defaultShippingZone {
+            id
+        }
+        defaultTaxZone {
+            id
+        }
+        pricesIncludeTax
+    }
+`;

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


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

@@ -122,6 +122,8 @@ export type AuthenticationMethod = Node & {
     strategy: Scalars['String'];
     strategy: Scalars['String'];
 };
 };
 
 
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
     name: Scalars['String'];
     name: Scalars['String'];
     type: Scalars['String'];
     type: Scalars['String'];
@@ -838,12 +840,12 @@ export enum ErrorCode {
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     PAYMENT_DECLINED_ERROR = 'PAYMENT_DECLINED_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     ALREADY_LOGGED_IN_ERROR = 'ALREADY_LOGGED_IN_ERROR',
     EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
     EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
-    MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
+    INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
     NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
     NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+    MISSING_PASSWORD_ERROR = 'MISSING_PASSWORD_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_INVALID_ERROR = 'VERIFICATION_TOKEN_INVALID_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     VERIFICATION_TOKEN_EXPIRED_ERROR = 'VERIFICATION_TOKEN_EXPIRED_ERROR',
     PASSWORD_ALREADY_SET_ERROR = 'PASSWORD_ALREADY_SET_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_INVALID_ERROR = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR',
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
@@ -1040,7 +1042,7 @@ export type IntCustomFieldConfig = CustomField & {
     step?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
 };
 };
 
 
-/** Returned if the user credentials are not valid */
+/** Returned if the user authentication credentials are not valid */
 export type InvalidCredentialsError = ErrorResult & {
 export type InvalidCredentialsError = ErrorResult & {
     code: ErrorCode;
     code: ErrorCode;
     message: Scalars['String'];
     message: Scalars['String'];
@@ -1394,10 +1396,6 @@ export enum LogicalOperator {
     OR = 'OR',
     OR = 'OR',
 }
 }
 
 
-export type LoginResult = {
-    user: CurrentUser;
-};
-
 /** Retured when attemting to register or verify a customer account without a password, when one is required. */
 /** Retured when attemting to register or verify a customer account without a password, when one is required. */
 export type MissingPasswordError = ErrorResult & {
 export type MissingPasswordError = ErrorResult & {
     code: ErrorCode;
     code: ErrorCode;
@@ -1441,11 +1439,11 @@ export type Mutation = {
      * Authenticates the user using the native authentication strategy. This mutation
      * Authenticates the user using the native authentication strategy. This mutation
      * is an alias for `authenticate({ native: { ... }})`
      * is an alias for `authenticate({ native: { ... }})`
      */
      */
-    login: LoginResult;
+    login: NativeAuthenticationResult;
     /** Authenticates the user using a named authentication strategy */
     /** Authenticates the user using a named authentication strategy */
-    authenticate: LoginResult;
+    authenticate: AuthenticationResult;
     /** End the current authenticated session */
     /** End the current authenticated session */
-    logout: Scalars['Boolean'];
+    logout: Success;
     /**
     /**
      * Register a Customer account with the given credentials. There are three possible registration flows:
      * Register a Customer account with the given credentials. There are three possible registration flows:
      *
      *
@@ -1622,6 +1620,8 @@ export type MutationResetPasswordArgs = {
     password: Scalars['String'];
     password: Scalars['String'];
 };
 };
 
 
+export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type NativeAuthInput = {
 export type NativeAuthInput = {
     username: Scalars['String'];
     username: Scalars['String'];
     password: Scalars['String'];
     password: Scalars['String'];

+ 43 - 27
packages/core/e2e/graphql/shared-definitions.ts

@@ -3,11 +3,13 @@ import gql from 'graphql-tag';
 import {
 import {
     ADMINISTRATOR_FRAGMENT,
     ADMINISTRATOR_FRAGMENT,
     ASSET_FRAGMENT,
     ASSET_FRAGMENT,
+    CHANNEL_FRAGMENT,
     COLLECTION_FRAGMENT,
     COLLECTION_FRAGMENT,
     COUNTRY_FRAGMENT,
     COUNTRY_FRAGMENT,
     CURRENT_USER_FRAGMENT,
     CURRENT_USER_FRAGMENT,
     CUSTOMER_FRAGMENT,
     CUSTOMER_FRAGMENT,
     FACET_WITH_VALUES_FRAGMENT,
     FACET_WITH_VALUES_FRAGMENT,
+    FULFILLMENT_FRAGMENT,
     ORDER_FRAGMENT,
     ORDER_FRAGMENT,
     ORDER_WITH_LINES_FRAGMENT,
     ORDER_WITH_LINES_FRAGMENT,
     PRODUCT_VARIANT_FRAGMENT,
     PRODUCT_VARIANT_FRAGMENT,
@@ -199,9 +201,7 @@ export const GET_CUSTOMER = gql`
 export const ATTEMPT_LOGIN = gql`
 export const ATTEMPT_LOGIN = gql`
     mutation AttemptLogin($username: String!, $password: String!, $rememberMe: Boolean) {
     mutation AttemptLogin($username: String!, $password: String!, $rememberMe: Boolean) {
         login(username: $username, password: $password, rememberMe: $rememberMe) {
         login(username: $username, password: $password, rememberMe: $rememberMe) {
-            user {
-                ...CurrentUser
-            }
+            ...CurrentUser
         }
         }
     }
     }
     ${CURRENT_USER_FRAGMENT}
     ${CURRENT_USER_FRAGMENT}
@@ -288,6 +288,10 @@ export const CREATE_PROMOTION = gql`
     mutation CreatePromotion($input: CreatePromotionInput!) {
     mutation CreatePromotion($input: CreatePromotionInput!) {
         createPromotion(input: $input) {
         createPromotion(input: $input) {
             ...Promotion
             ...Promotion
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
     ${PROMOTION_FRAGMENT}
     ${PROMOTION_FRAGMENT}
@@ -304,20 +308,15 @@ export const ME = gql`
 export const CREATE_CHANNEL = gql`
 export const CREATE_CHANNEL = gql`
     mutation CreateChannel($input: CreateChannelInput!) {
     mutation CreateChannel($input: CreateChannelInput!) {
         createChannel(input: $input) {
         createChannel(input: $input) {
-            id
-            code
-            token
-            currencyCode
-            defaultLanguageCode
-            defaultShippingZone {
-                id
-            }
-            defaultTaxZone {
-                id
+            ...Channel
+            ... on LanguageNotAvailableError {
+                errorCode: code
+                message
+                languageCode
             }
             }
-            pricesIncludeTax
         }
         }
     }
     }
+    ${CHANNEL_FRAGMENT}
 `;
 `;
 
 
 export const DELETE_PRODUCT_VARIANT = gql`
 export const DELETE_PRODUCT_VARIANT = gql`
@@ -374,12 +373,15 @@ export const DELETE_ASSET = gql`
 export const UPDATE_CHANNEL = gql`
 export const UPDATE_CHANNEL = gql`
     mutation UpdateChannel($input: UpdateChannelInput!) {
     mutation UpdateChannel($input: UpdateChannelInput!) {
         updateChannel(input: $input) {
         updateChannel(input: $input) {
-            id
-            code
-            defaultLanguageCode
-            currencyCode
+            ...Channel
+            ... on LanguageNotAvailableError {
+                errorCode: code
+                message
+                languageCode
+            }
         }
         }
     }
     }
+    ${CHANNEL_FRAGMENT}
 `;
 `;
 
 
 export const GET_CUSTOMER_HISTORY = gql`
 export const GET_CUSTOMER_HISTORY = gql`
@@ -443,25 +445,31 @@ export const REMOVE_CUSTOMERS_FROM_GROUP = gql`
 
 
 export const CREATE_FULFILLMENT = gql`
 export const CREATE_FULFILLMENT = gql`
     mutation CreateFulfillment($input: FulfillOrderInput!) {
     mutation CreateFulfillment($input: FulfillOrderInput!) {
-        fulfillOrder(input: $input) {
-            id
-            method
-            state
-            trackingCode
-            orderItems {
-                id
+        addFulfillmentToOrder(input: $input) {
+            ...Fulfillment
+            ... on ErrorResult {
+                code
+                message
             }
             }
         }
         }
     }
     }
+    ${FULFILLMENT_FRAGMENT}
 `;
 `;
 
 
 export const TRANSIT_FULFILLMENT = gql`
 export const TRANSIT_FULFILLMENT = gql`
     mutation TransitFulfillment($id: ID!, $state: String!) {
     mutation TransitFulfillment($id: ID!, $state: String!) {
         transitionFulfillmentToState(id: $id, state: $state) {
         transitionFulfillmentToState(id: $id, state: $state) {
-            id
-            state
+            ...Fulfillment
+            ... on FulfillmentStateTransitionError {
+                code
+                message
+                transitionError
+                fromState
+                toState
+            }
         }
         }
     }
     }
+    ${FULFILLMENT_FRAGMENT}
 `;
 `;
 
 
 export const GET_ORDER_FULFILLMENTS = gql`
 export const GET_ORDER_FULFILLMENTS = gql`
@@ -531,6 +539,10 @@ export const CREATE_CUSTOMER = gql`
     mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
     mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
         createCustomer(input: $input, password: $password) {
         createCustomer(input: $input, password: $password) {
             ...Customer
             ...Customer
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
     ${CUSTOMER_FRAGMENT}
     ${CUSTOMER_FRAGMENT}
@@ -540,6 +552,10 @@ export const UPDATE_CUSTOMER = gql`
     mutation UpdateCustomer($input: UpdateCustomerInput!) {
     mutation UpdateCustomer($input: UpdateCustomerInput!) {
         updateCustomer(input: $input) {
         updateCustomer(input: $input) {
             ...Customer
             ...Customer
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
     ${CUSTOMER_FRAGMENT}
     ${CUSTOMER_FRAGMENT}

+ 14 - 5
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -25,6 +25,7 @@ import {
     GetFacetList,
     GetFacetList,
     GetPromoProducts,
     GetPromoProducts,
     HistoryEntryType,
     HistoryEntryType,
+    PromotionFragment,
     RemoveCustomersFromGroup,
     RemoveCustomersFromGroup,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import {
 import {
@@ -39,6 +40,7 @@ import {
     SetCustomerForOrder,
     SetCustomerForOrder,
     TestOrderFragment,
     TestOrderFragment,
     TestOrderFragmentFragment,
     TestOrderFragmentFragment,
+    TestOrderWithPaymentsFragment,
     UpdatedOrderFragment,
     UpdatedOrderFragment,
 } from './graphql/generated-e2e-shop-types';
 } from './graphql/generated-e2e-shop-types';
 import {
 import {
@@ -105,8 +107,8 @@ describe('Promotions applied to Orders', () => {
     describe('coupon codes', () => {
     describe('coupon codes', () => {
         const TEST_COUPON_CODE = 'TESTCOUPON';
         const TEST_COUPON_CODE = 'TESTCOUPON';
         const EXPIRED_COUPON_CODE = 'EXPIRED';
         const EXPIRED_COUPON_CODE = 'EXPIRED';
-        let promoFreeWithCoupon: CreatePromotion.CreatePromotion;
-        let promoFreeWithExpiredCoupon: CreatePromotion.CreatePromotion;
+        let promoFreeWithCoupon: PromotionFragment;
+        let promoFreeWithExpiredCoupon: PromotionFragment;
 
 
         beforeAll(async () => {
         beforeAll(async () => {
             promoFreeWithCoupon = await createPromotion({
             promoFreeWithCoupon = await createPromotion({
@@ -696,7 +698,10 @@ describe('Promotions applied to Orders', () => {
 
 
     describe('per-customer usage limit', () => {
     describe('per-customer usage limit', () => {
         const TEST_COUPON_CODE = 'TESTCOUPON';
         const TEST_COUPON_CODE = 'TESTCOUPON';
-        let promoWithUsageLimit: CreatePromotion.CreatePromotion;
+        const orderGuard: ErrorResultGuard<TestOrderWithPaymentsFragment> = createErrorResultGuard<
+            TestOrderWithPaymentsFragment
+        >(input => !!input.lines);
+        let promoWithUsageLimit: PromotionFragment;
 
 
         beforeAll(async () => {
         beforeAll(async () => {
             promoWithUsageLimit = await createPromotion({
             promoWithUsageLimit = await createPromotion({
@@ -758,6 +763,8 @@ describe('Promotions applied to Orders', () => {
 
 
                 await proceedToArrangingPayment(shopClient);
                 await proceedToArrangingPayment(shopClient);
                 const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
                 const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+                orderGuard.assertSuccess(order);
+
                 expect(order.state).toBe('PaymentSettled');
                 expect(order.state).toBe('PaymentSettled');
                 expect(order.active).toBe(false);
                 expect(order.active).toBe(false);
                 orderCode = order.code;
                 orderCode = order.code;
@@ -831,6 +838,8 @@ describe('Promotions applied to Orders', () => {
 
 
                 await proceedToArrangingPayment(shopClient);
                 await proceedToArrangingPayment(shopClient);
                 const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
                 const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
+                orderGuard.assertSuccess(order);
+
                 expect(order.state).toBe('PaymentSettled');
                 expect(order.state).toBe('PaymentSettled');
                 expect(order.active).toBe(false);
                 expect(order.active).toBe(false);
             });
             });
@@ -899,14 +908,14 @@ describe('Promotions applied to Orders', () => {
         await deletePromotion(deletedPromotion.id);
         await deletePromotion(deletedPromotion.id);
     }
     }
 
 
-    async function createPromotion(input: CreatePromotionInput): Promise<CreatePromotion.CreatePromotion> {
+    async function createPromotion(input: CreatePromotionInput): Promise<PromotionFragment> {
         const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
         const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
             CREATE_PROMOTION,
             CREATE_PROMOTION,
             {
             {
                 input,
                 input,
             },
             },
         );
         );
-        return result.createPromotion;
+        return result.createPromotion as PromotionFragment;
     }
     }
 
 
     function getVariantBySlug(
     function getVariantBySlug(

+ 363 - 241
packages/core/e2e/order.e2e-spec.ts

@@ -1,6 +1,11 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
-import { createTestEnvironment, SimpleGraphQLClient } from '@vendure/testing';
+import {
+    createErrorResultGuard,
+    createTestEnvironment,
+    ErrorResultGuard,
+    SimpleGraphQLClient,
+} from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
@@ -13,11 +18,15 @@ import {
     singleStageRefundablePaymentMethod,
     singleStageRefundablePaymentMethod,
     twoStagePaymentMethod,
     twoStagePaymentMethod,
 } from './fixtures/test-payment-methods';
 } from './fixtures/test-payment-methods';
+import { FULFILLMENT_FRAGMENT } from './graphql/fragments';
 import {
 import {
     AddNoteToOrder,
     AddNoteToOrder,
+    CanceledOrderFragment,
     CancelOrder,
     CancelOrder,
     CreateFulfillment,
     CreateFulfillment,
     DeleteOrderNote,
     DeleteOrderNote,
+    ErrorCode,
+    FulfillmentFragment,
     GetCustomerList,
     GetCustomerList,
     GetOrder,
     GetOrder,
     GetOrderFulfillmentItems,
     GetOrderFulfillmentItems,
@@ -28,6 +37,8 @@ import {
     GetProductWithVariants,
     GetProductWithVariants,
     GetStockMovement,
     GetStockMovement,
     HistoryEntryType,
     HistoryEntryType,
+    PaymentFragment,
+    RefundFragment,
     RefundOrder,
     RefundOrder,
     SettlePayment,
     SettlePayment,
     SettleRefund,
     SettleRefund,
@@ -37,7 +48,13 @@ import {
     UpdateOrderNote,
     UpdateOrderNote,
     UpdateProductVariants,
     UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
-import { AddItemToOrder, DeletionResult, GetActiveOrder } from './graphql/generated-e2e-shop-types';
+import {
+    AddItemToOrder,
+    DeletionResult,
+    GetActiveOrder,
+    TestOrderFragmentFragment,
+    UpdatedOrder,
+} from './graphql/generated-e2e-shop-types';
 import {
 import {
     CREATE_FULFILLMENT,
     CREATE_FULFILLMENT,
     GET_CUSTOMER_LIST,
     GET_CUSTOMER_LIST,
@@ -67,6 +84,19 @@ describe('Orders resolver', () => {
     let customers: GetCustomerList.Items[];
     let customers: GetCustomerList.Items[];
     const password = 'test';
     const password = 'test';
 
 
+    const orderGuard: ErrorResultGuard<
+        TestOrderFragmentFragment | CanceledOrderFragment
+    > = createErrorResultGuard<TestOrderFragmentFragment | CanceledOrderFragment>(input => !!input.lines);
+    const paymentGuard: ErrorResultGuard<PaymentFragment> = createErrorResultGuard<PaymentFragment>(
+        input => !!input.state,
+    );
+    const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard<
+        FulfillmentFragment
+    >(input => !!input.method);
+    const refundGuard: ErrorResultGuard<RefundFragment> = createErrorResultGuard<RefundFragment>(
+        input => !!input.items,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -133,6 +163,7 @@ describe('Orders resolver', () => {
             await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
             await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
             await proceedToArrangingPayment(shopClient);
             await proceedToArrangingPayment(shopClient);
             const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
             const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
+            orderGuard.assertSuccess(order);
 
 
             expect(order.state).toBe('PaymentAuthorized');
             expect(order.state).toBe('PaymentAuthorized');
 
 
@@ -143,9 +174,11 @@ describe('Orders resolver', () => {
             >(SETTLE_PAYMENT, {
             >(SETTLE_PAYMENT, {
                 id: payment.id,
                 id: payment.id,
             });
             });
+            paymentGuard.assertErrorResult(settlePayment);
 
 
-            expect(settlePayment!.id).toBe(payment.id);
-            expect(settlePayment!.state).toBe('Authorized');
+            expect(settlePayment.message).toBe('Settling the payment failed');
+            expect(settlePayment.code).toBe(ErrorCode.SETTLE_PAYMENT_ERROR);
+            expect((settlePayment as any).paymentErrorMessage).toBe('Something went horribly wrong');
 
 
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: order.id,
                 id: order.id,
@@ -159,6 +192,7 @@ describe('Orders resolver', () => {
             await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
             await shopClient.asUserWithCredentials(customers[1].emailAddress, password);
             await proceedToArrangingPayment(shopClient);
             await proceedToArrangingPayment(shopClient);
             const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
             const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
+            orderGuard.assertSuccess(order);
 
 
             expect(order.state).toBe('PaymentAuthorized');
             expect(order.state).toBe('PaymentAuthorized');
             expect(onTransitionSpy).toHaveBeenCalledTimes(1);
             expect(onTransitionSpy).toHaveBeenCalledTimes(1);
@@ -172,6 +206,7 @@ describe('Orders resolver', () => {
             >(SETTLE_PAYMENT, {
             >(SETTLE_PAYMENT, {
                 id: payment.id,
                 id: payment.id,
             });
             });
+            paymentGuard.assertSuccess(settlePayment);
 
 
             expect(settlePayment!.id).toBe(payment.id);
             expect(settlePayment!.id).toBe(payment.id);
             expect(settlePayment!.state).toBe('Settled');
             expect(settlePayment!.state).toBe('Settled');
@@ -240,63 +275,45 @@ describe('Orders resolver', () => {
     });
     });
 
 
     describe('fulfillment', () => {
     describe('fulfillment', () => {
-        it(
-            'throws if Order is not in "PaymentSettled" state',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: 'T_1',
-                });
-                expect(order!.state).toBe('PaymentAuthorized');
+        it('return error result if lines is empty', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+            expect(order!.state).toBe('PaymentSettled');
+            const { addFulfillmentToOrder } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    lines: [],
+                    method: 'Test',
+                },
+            });
+            fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
 
 
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
-                    CREATE_FULFILLMENT,
-                    {
-                        input: {
-                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
-                            method: 'Test',
-                        },
-                    },
-                );
-            }, 'One or more OrderItems belong to an Order which is in an invalid state'),
-        );
+            expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
+            expect(addFulfillmentToOrder.code).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
+        });
 
 
-        it(
-            'throws if lines is empty',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: 'T_2',
-                });
-                expect(order!.state).toBe('PaymentSettled');
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
-                    CREATE_FULFILLMENT,
-                    {
-                        input: {
-                            lines: [],
-                            method: 'Test',
-                        },
-                    },
-                );
-            }, 'Nothing to fulfill'),
-        );
+        it('returns error result if all quantities are zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+            expect(order!.state).toBe('PaymentSettled');
+            const { addFulfillmentToOrder } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
+                    method: 'Test',
+                },
+            });
+            fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
 
 
-        it(
-            'throws if all quantities are zero',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: 'T_2',
-                });
-                expect(order!.state).toBe('PaymentSettled');
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
-                    CREATE_FULFILLMENT,
-                    {
-                        input: {
-                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
-                            method: 'Test',
-                        },
-                    },
-                );
-            }, 'Nothing to fulfill'),
-        );
+            expect(addFulfillmentToOrder.message).toBe('At least one OrderLine must be specified');
+            expect(addFulfillmentToOrder.code).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
+        });
 
 
         it('creates the first fulfillment', async () => {
         it('creates the first fulfillment', async () => {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
@@ -305,7 +322,7 @@ describe('Orders resolver', () => {
             expect(order!.state).toBe('PaymentSettled');
             expect(order!.state).toBe('PaymentSettled');
             const lines = order!.lines;
             const lines = order!.lines;
 
 
-            const { fulfillOrder } = await adminClient.query<
+            const { addFulfillmentToOrder } = await adminClient.query<
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Variables
                 CreateFulfillment.Variables
             >(CREATE_FULFILLMENT, {
             >(CREATE_FULFILLMENT, {
@@ -315,21 +332,22 @@ describe('Orders resolver', () => {
                     trackingCode: '111',
                     trackingCode: '111',
                 },
                 },
             });
             });
+            fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
 
 
-            expect(fulfillOrder!.id).toBe('T_1');
-            expect(fulfillOrder!.method).toBe('Test1');
-            expect(fulfillOrder!.trackingCode).toBe('111');
-            expect(fulfillOrder!.state).toBe('Pending');
-            expect(fulfillOrder!.orderItems).toEqual([{ id: lines[0].items[0].id }]);
+            expect(addFulfillmentToOrder.id).toBe('T_1');
+            expect(addFulfillmentToOrder.method).toBe('Test1');
+            expect(addFulfillmentToOrder.trackingCode).toBe('111');
+            expect(addFulfillmentToOrder.state).toBe('Pending');
+            expect(addFulfillmentToOrder.orderItems).toEqual([{ id: lines[0].items[0].id }]);
 
 
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: 'T_2',
                 id: 'T_2',
             });
             });
 
 
-            expect(result.order!.lines[0].items[0].fulfillment!.id).toBe(fulfillOrder!.id);
+            expect(result.order!.lines[0].items[0].fulfillment!.id).toBe(addFulfillmentToOrder!.id);
             expect(
             expect(
                 result.order!.lines[1].items.filter(
                 result.order!.lines[1].items.filter(
-                    i => i.fulfillment && i.fulfillment.id === fulfillOrder.id,
+                    i => i.fulfillment && i.fulfillment.id === addFulfillmentToOrder.id,
                 ).length,
                 ).length,
             ).toBe(0);
             ).toBe(0);
             expect(result.order!.lines[1].items.filter(i => i.fulfillment == null).length).toBe(3);
             expect(result.order!.lines[1].items.filter(i => i.fulfillment == null).length).toBe(3);
@@ -346,7 +364,7 @@ describe('Orders resolver', () => {
                     return items.length > 0 ? true : false;
                     return items.length > 0 ? true : false;
                 }) || [];
                 }) || [];
 
 
-            const { fulfillOrder } = await adminClient.query<
+            const { addFulfillmentToOrder } = await adminClient.query<
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Mutation,
                 CreateFulfillment.Variables
                 CreateFulfillment.Variables
             >(CREATE_FULFILLMENT, {
             >(CREATE_FULFILLMENT, {
@@ -359,93 +377,109 @@ describe('Orders resolver', () => {
                     trackingCode: '222',
                     trackingCode: '222',
                 },
                 },
             });
             });
+            fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
 
 
-            expect(fulfillOrder!.id).toBe('T_2');
-            expect(fulfillOrder!.method).toBe('Test2');
-            expect(fulfillOrder!.trackingCode).toBe('222');
-            expect(fulfillOrder!.state).toBe('Pending');
+            expect(addFulfillmentToOrder.id).toBe('T_2');
+            expect(addFulfillmentToOrder.method).toBe('Test2');
+            expect(addFulfillmentToOrder.trackingCode).toBe('222');
+            expect(addFulfillmentToOrder.state).toBe('Pending');
         });
         });
 
 
-        it(
-            'throws if an OrderItem already part of a Fulfillment',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: 'T_2',
-                });
-                await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
-                    CREATE_FULFILLMENT,
-                    {
-                        input: {
-                            method: 'Test',
-                            lines: [
-                                {
-                                    orderLineId: order!.lines[0].id,
-                                    quantity: 1,
-                                },
-                            ],
+        it('returns error result if an OrderItem already part of a Fulfillment', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: 'T_2',
+            });
+            const { addFulfillmentToOrder } = await adminClient.query<
+                CreateFulfillment.Mutation,
+                CreateFulfillment.Variables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    method: 'Test',
+                    lines: [
+                        {
+                            orderLineId: order!.lines[0].id,
+                            quantity: 1,
                         },
                         },
-                    },
-                );
-            }, 'One or more OrderItems have already been fulfilled'),
-        );
+                    ],
+                },
+            });
+            fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
+
+            expect(addFulfillmentToOrder.message).toBe(
+                'One or more OrderItems are already part of a Fulfillment',
+            );
+            expect(addFulfillmentToOrder.code).toBe(ErrorCode.ITEMS_ALREADY_FULFILLED_ERROR);
+        });
+
         it('transits the first fulfillment from created to Shipped and automatically change the order state to PartiallyShipped', async () => {
         it('transits the first fulfillment from created to Shipped and automatically change the order state to PartiallyShipped', async () => {
-            const fulfillment = await adminClient.query<
+            const { transitionFulfillmentToState } = await adminClient.query<
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Variables
                 TransitFulfillment.Variables
             >(TRANSIT_FULFILLMENT, {
             >(TRANSIT_FULFILLMENT, {
                 id: 'T_1',
                 id: 'T_1',
                 state: 'Shipped',
                 state: 'Shipped',
             });
             });
-            expect(fulfillment.transitionFulfillmentToState?.id).toBe('T_1');
-            expect(fulfillment.transitionFulfillmentToState?.state).toBe('Shipped');
+            fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
+
+            expect(transitionFulfillmentToState.id).toBe('T_1');
+            expect(transitionFulfillmentToState.state).toBe('Shipped');
 
 
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: 'T_2',
                 id: 'T_2',
             });
             });
             expect(order?.state).toBe('PartiallyShipped');
             expect(order?.state).toBe('PartiallyShipped');
         });
         });
+
         it('transits the second fulfillment from created to Shipped and automatically change the order state to Shipped', async () => {
         it('transits the second fulfillment from created to Shipped and automatically change the order state to Shipped', async () => {
-            const fulfillment = await adminClient.query<
+            const { transitionFulfillmentToState } = await adminClient.query<
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Variables
                 TransitFulfillment.Variables
             >(TRANSIT_FULFILLMENT, {
             >(TRANSIT_FULFILLMENT, {
                 id: 'T_2',
                 id: 'T_2',
                 state: 'Shipped',
                 state: 'Shipped',
             });
             });
-            expect(fulfillment.transitionFulfillmentToState?.id).toBe('T_2');
-            expect(fulfillment.transitionFulfillmentToState?.state).toBe('Shipped');
+            fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
+
+            expect(transitionFulfillmentToState.id).toBe('T_2');
+            expect(transitionFulfillmentToState.state).toBe('Shipped');
 
 
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: 'T_2',
                 id: 'T_2',
             });
             });
             expect(order?.state).toBe('Shipped');
             expect(order?.state).toBe('Shipped');
         });
         });
+
         it('transits the first fulfillment from Shipped to Delivered and change the order state to PartiallyDelivered', async () => {
         it('transits the first fulfillment from Shipped to Delivered and change the order state to PartiallyDelivered', async () => {
-            const fulfillment = await adminClient.query<
+            const { transitionFulfillmentToState } = await adminClient.query<
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Variables
                 TransitFulfillment.Variables
             >(TRANSIT_FULFILLMENT, {
             >(TRANSIT_FULFILLMENT, {
                 id: 'T_1',
                 id: 'T_1',
                 state: 'Delivered',
                 state: 'Delivered',
             });
             });
-            expect(fulfillment.transitionFulfillmentToState?.id).toBe('T_1');
-            expect(fulfillment.transitionFulfillmentToState?.state).toBe('Delivered');
+            fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
+
+            expect(transitionFulfillmentToState.id).toBe('T_1');
+            expect(transitionFulfillmentToState.state).toBe('Delivered');
 
 
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: 'T_2',
                 id: 'T_2',
             });
             });
             expect(order?.state).toBe('PartiallyDelivered');
             expect(order?.state).toBe('PartiallyDelivered');
         });
         });
+
         it('transits the second fulfillment from Shipped to Delivered and change the order state to Delivered', async () => {
         it('transits the second fulfillment from Shipped to Delivered and change the order state to Delivered', async () => {
-            const fulfillment = await adminClient.query<
+            const { transitionFulfillmentToState } = await adminClient.query<
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Mutation,
                 TransitFulfillment.Variables
                 TransitFulfillment.Variables
             >(TRANSIT_FULFILLMENT, {
             >(TRANSIT_FULFILLMENT, {
                 id: 'T_2',
                 id: 'T_2',
                 state: 'Delivered',
                 state: 'Delivered',
             });
             });
-            expect(fulfillment.transitionFulfillmentToState?.id).toBe('T_2');
-            expect(fulfillment.transitionFulfillmentToState?.state).toBe('Delivered');
+            fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
+
+            expect(transitionFulfillmentToState.id).toBe('T_2');
+            expect(transitionFulfillmentToState.state).toBe('Delivered');
 
 
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: 'T_2',
                 id: 'T_2',
@@ -642,6 +676,8 @@ describe('Orders resolver', () => {
             );
             );
             await proceedToArrangingPayment(shopClient);
             await proceedToArrangingPayment(shopClient);
             const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
             const order = await addPaymentToOrder(shopClient, failsToSettlePaymentMethod);
+            orderGuard.assertSuccess(order);
+
             expect(order.state).toBe('PaymentAuthorized');
             expect(order.state).toBe('PaymentAuthorized');
 
 
             const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
             const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
@@ -665,6 +701,8 @@ describe('Orders resolver', () => {
                     },
                     },
                 },
                 },
             );
             );
+            orderGuard.assertSuccess(cancelOrder);
+
             expect(
             expect(
                 cancelOrder.lines.map(l =>
                 cancelOrder.lines.map(l =>
                     l.items.map(pick(['id', 'cancelled'])).sort((a, b) => (a.id > b.id ? 1 : -1)),
                     l.items.map(pick(['id', 'cancelled'])).sort((a, b) => (a.id > b.id ? 1 : -1)),
@@ -729,68 +767,91 @@ describe('Orders resolver', () => {
             productVariantId = result.productVariantId;
             productVariantId = result.productVariantId;
         });
         });
 
 
-        it(
-            'cannot cancel from AddingItems state',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                expect(order!.state).toBe('AddingItems');
-                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+        it('cannot cancel from AddingItems state', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            expect(order!.state).toBe('AddingItems');
+
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
+                CANCEL_ORDER,
+                {
                     input: {
                     input: {
                         orderId,
                         orderId,
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     },
                     },
-                });
-            }, 'Cannot cancel OrderLines from an Order in the "AddingItems" state'),
-        );
+                },
+            );
+            orderGuard.assertErrorResult(cancelOrder);
 
 
-        it(
-            'cannot cancel from ArrangingPayment state',
-            assertThrowsWithMessage(async () => {
-                await proceedToArrangingPayment(shopClient);
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                expect(order!.state).toBe('ArrangingPayment');
-                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+            expect(cancelOrder.message).toBe(
+                'Cannot cancel OrderLines from an Order in the "AddingItems" state',
+            );
+            expect(cancelOrder.code).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
+        });
+
+        it('cannot cancel from ArrangingPayment state', async () => {
+            await proceedToArrangingPayment(shopClient);
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            expect(order!.state).toBe('ArrangingPayment');
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
+                CANCEL_ORDER,
+                {
                     input: {
                     input: {
                         orderId,
                         orderId,
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                     },
                     },
-                });
-            }, 'Cannot cancel OrderLines from an Order in the "ArrangingPayment" state'),
-        );
+                },
+            );
+            orderGuard.assertErrorResult(cancelOrder);
 
 
-        it(
-            'throws if lines are empty',
-            assertThrowsWithMessage(async () => {
-                const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
-                expect(order.state).toBe('PaymentAuthorized');
+            expect(cancelOrder.message).toBe(
+                'Cannot cancel OrderLines from an Order in the "ArrangingPayment" state',
+            );
+            expect(cancelOrder.code).toBe(ErrorCode.CANCEL_ACTIVE_ORDER_ERROR);
+        });
 
 
-                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+        it('returns error result if lines are empty', async () => {
+            const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
+            orderGuard.assertSuccess(order);
+
+            expect(order.state).toBe('PaymentAuthorized');
+
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
+                CANCEL_ORDER,
+                {
                     input: {
                     input: {
                         orderId,
                         orderId,
                         lines: [],
                         lines: [],
                     },
                     },
-                });
-            }, 'Nothing to cancel'),
-        );
+                },
+            );
+            orderGuard.assertErrorResult(cancelOrder);
 
 
-        it(
-            'throws if all quantities zero',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
+            expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
+            expect(cancelOrder.code).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
+        });
+
+        it('returns error result if all quantities zero', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
+                CANCEL_ORDER,
+                {
                     input: {
                     input: {
                         orderId,
                         orderId,
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
                     },
                     },
-                });
-            }, 'Nothing to cancel'),
-        );
+                },
+            );
+            orderGuard.assertErrorResult(cancelOrder);
+
+            expect(cancelOrder.message).toBe('At least one OrderLine must be specified');
+            expect(cancelOrder.code).toBe(ErrorCode.EMPTY_ORDER_LINE_SELECTION_ERROR);
+        });
 
 
         it('partial cancellation', async () => {
         it('partial cancellation', async () => {
             const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
             const result1 = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
@@ -823,6 +884,7 @@ describe('Orders resolver', () => {
                     },
                     },
                 },
                 },
             );
             );
+            orderGuard.assertSuccess(cancelOrder);
 
 
             expect(cancelOrder.lines[0].quantity).toBe(1);
             expect(cancelOrder.lines[0].quantity).toBe(1);
             expect(cancelOrder.lines[0].items.sort((a, b) => (a.id < b.id ? -1 : 1))).toEqual([
             expect(cancelOrder.lines[0].items.sort((a, b) => (a.id < b.id ? -1 : 1))).toEqual([
@@ -855,6 +917,27 @@ describe('Orders resolver', () => {
             ]);
             ]);
         });
         });
 
 
+        it('returns error result if attempting to cancel already cancelled item', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(
+                CANCEL_ORDER,
+                {
+                    input: {
+                        orderId,
+                        lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 2 })),
+                    },
+                },
+            );
+            orderGuard.assertErrorResult(cancelOrder);
+
+            expect(cancelOrder.message).toBe(
+                'The specified quantity is greater than the available OrderItems',
+            );
+            expect(cancelOrder.code).toBe(ErrorCode.QUANTITY_TOO_GREAT_ERROR);
+        });
+
         it('complete cancellation', async () => {
         it('complete cancellation', async () => {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
                 id: orderId,
                 id: orderId,
@@ -968,50 +1051,61 @@ describe('Orders resolver', () => {
             productVariantId = result.productVariantId;
             productVariantId = result.productVariantId;
         });
         });
 
 
-        it(
-            'cannot refund from PaymentAuthorized state',
-            assertThrowsWithMessage(async () => {
-                await proceedToArrangingPayment(shopClient);
-                const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
-                expect(order.state).toBe('PaymentAuthorized');
-                paymentId = order.payments![0].id;
+        it('cannot refund from PaymentAuthorized state', async () => {
+            await proceedToArrangingPayment(shopClient);
+            const order = await addPaymentToOrder(shopClient, twoStagePaymentMethod);
+            orderGuard.assertSuccess(order);
 
 
-                await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
+            expect(order.state).toBe('PaymentAuthorized');
+            paymentId = order.payments![0].id;
+
+            const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
+                REFUND_ORDER,
+                {
                     input: {
                     input: {
                         lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                         lines: order.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
                         shipping: 0,
                         shipping: 0,
                         adjustment: 0,
                         adjustment: 0,
                         paymentId,
                         paymentId,
                     },
                     },
-                });
-            }, 'Cannot refund an Order in the "PaymentAuthorized" state'),
-        );
+                },
+            );
+            refundGuard.assertErrorResult(refundOrder);
 
 
-        it(
-            'throws if no lines and no shipping',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                const { settlePayment } = await adminClient.query<
-                    SettlePayment.Mutation,
-                    SettlePayment.Variables
-                >(SETTLE_PAYMENT, {
-                    id: order!.payments![0].id,
-                });
+            expect(refundOrder.message).toBe('Cannot refund an Order in the "PaymentAuthorized" state');
+            expect(refundOrder.code).toBe(ErrorCode.REFUND_ORDER_STATE_ERROR);
+        });
 
 
-                expect(settlePayment!.state).toBe('Settled');
+        it('returns error result if no lines and no shipping', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { settlePayment } = await adminClient.query<
+                SettlePayment.Mutation,
+                SettlePayment.Variables
+            >(SETTLE_PAYMENT, {
+                id: order!.payments![0].id,
+            });
+            paymentGuard.assertSuccess(settlePayment);
 
 
-                await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(REFUND_ORDER, {
+            expect(settlePayment!.state).toBe('Settled');
+
+            const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
+                REFUND_ORDER,
+                {
                     input: {
                     input: {
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
                         lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 0 })),
                         shipping: 0,
                         shipping: 0,
                         adjustment: 0,
                         adjustment: 0,
                         paymentId,
                         paymentId,
                     },
                     },
-                });
-            }, 'Nothing to refund'),
-        );
+                },
+            );
+            refundGuard.assertErrorResult(refundOrder);
+
+            expect(refundOrder.message).toBe('Nothing to refund');
+            expect(refundOrder.code).toBe(ErrorCode.NOTHING_TO_REFUND_ERROR);
+        });
 
 
         it(
         it(
             'throws if paymentId not valid',
             'throws if paymentId not valid',
@@ -1030,28 +1124,29 @@ describe('Orders resolver', () => {
                         },
                         },
                     },
                     },
                 );
                 );
-            }, "No Payment with the id '999' could be found"),
+            }, `No Payment with the id '999' could be found`),
         );
         );
 
 
-        it(
-            'throws if payment and order lines do not belong to the same Order',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
-                    REFUND_ORDER,
-                    {
-                        input: {
-                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
-                            shipping: 100,
-                            adjustment: 0,
-                            paymentId: 'T_1',
-                        },
+        it('returns error result if payment and order lines do not belong to the same Order', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
+                REFUND_ORDER,
+                {
+                    input: {
+                        lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
+                        shipping: 100,
+                        adjustment: 0,
+                        paymentId: 'T_1',
                     },
                     },
-                );
-            }, 'The Payment and OrderLines do not belong to the same Order'),
-        );
+                },
+            );
+            refundGuard.assertErrorResult(refundOrder);
+
+            expect(refundOrder.message).toBe('The Payment and OrderLines do not belong to the same Order');
+            expect(refundOrder.code).toBe(ErrorCode.PAYMENT_ORDER_MISMATCH_ERROR);
+        });
 
 
         it('creates a Refund to be manually settled', async () => {
         it('creates a Refund to be manually settled', async () => {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
             const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
@@ -1069,6 +1164,7 @@ describe('Orders resolver', () => {
                     },
                     },
                 },
                 },
             );
             );
+            refundGuard.assertSuccess(refundOrder);
 
 
             expect(refundOrder.shipping).toBe(order!.shipping);
             expect(refundOrder.shipping).toBe(order!.shipping);
             expect(refundOrder.items).toBe(order!.subTotal);
             expect(refundOrder.items).toBe(order!.subTotal);
@@ -1078,25 +1174,26 @@ describe('Orders resolver', () => {
             refundId = refundOrder.id;
             refundId = refundOrder.id;
         });
         });
 
 
-        it(
-            'throws if attempting to refund the same item more than once',
-            assertThrowsWithMessage(async () => {
-                const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
-                    id: orderId,
-                });
-                const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
-                    REFUND_ORDER,
-                    {
-                        input: {
-                            lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
-                            shipping: order!.shipping,
-                            adjustment: 0,
-                            paymentId,
-                        },
+        it('returns error result if attempting to refund the same item more than once', async () => {
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+            const { refundOrder } = await adminClient.query<RefundOrder.Mutation, RefundOrder.Variables>(
+                REFUND_ORDER,
+                {
+                    input: {
+                        lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
+                        shipping: order!.shipping,
+                        adjustment: 0,
+                        paymentId,
                     },
                     },
-                );
-            }, 'Cannot refund an OrderItem which has already been refunded'),
-        );
+                },
+            );
+            refundGuard.assertErrorResult(refundOrder);
+
+            expect(refundOrder.message).toBe('Cannot refund an OrderItem which has already been refunded');
+            expect(refundOrder.code).toBe(ErrorCode.ALREADY_REFUNDED_ERROR);
+        });
 
 
         it('manually settle a Refund', async () => {
         it('manually settle a Refund', async () => {
             const { settleRefund } = await adminClient.query<SettleRefund.Mutation, SettleRefund.Variables>(
             const { settleRefund } = await adminClient.query<SettleRefund.Mutation, SettleRefund.Variables>(
@@ -1108,6 +1205,7 @@ describe('Orders resolver', () => {
                     },
                     },
                 },
                 },
             );
             );
+            refundGuard.assertSuccess(settleRefund);
 
 
             expect(settleRefund.state).toBe('Settled');
             expect(settleRefund.state).toBe('Settled');
             expect(settleRefund.transactionId).toBe('aaabbb');
             expect(settleRefund.transactionId).toBe('aaabbb');
@@ -1356,18 +1454,28 @@ async function createTestOrder(
             quantity: 2,
             quantity: 2,
         },
         },
     );
     );
-    const orderId = addItemToOrder!.id;
+    const orderId = (addItemToOrder as UpdatedOrder.Fragment).id;
     return { product, productVariantId, orderId };
     return { product, productVariantId, orderId };
 }
 }
 
 
 export const SETTLE_PAYMENT = gql`
 export const SETTLE_PAYMENT = gql`
     mutation SettlePayment($id: ID!) {
     mutation SettlePayment($id: ID!) {
         settlePayment(id: $id) {
         settlePayment(id: $id) {
-            id
-            state
-            metadata
+            ...Payment
+            ... on ErrorResult {
+                code
+                message
+            }
+            ... on SettlePaymentError {
+                paymentErrorMessage
+            }
         }
         }
     }
     }
+    fragment Payment on Payment {
+        id
+        state
+        metadata
+    }
 `;
 `;
 
 
 export const GET_ORDER_LIST_FULFILLMENTS = gql`
 export const GET_ORDER_LIST_FULFILLMENTS = gql`
@@ -1393,57 +1501,71 @@ export const GET_ORDER_FULFILLMENT_ITEMS = gql`
             id
             id
             state
             state
             fulfillments {
             fulfillments {
-                id
-                state
-                orderItems {
-                    id
-                }
+                ...Fulfillment
             }
             }
         }
         }
     }
     }
+    ${FULFILLMENT_FRAGMENT}
 `;
 `;
 
 
 export const CANCEL_ORDER = gql`
 export const CANCEL_ORDER = gql`
     mutation CancelOrder($input: CancelOrderInput!) {
     mutation CancelOrder($input: CancelOrderInput!) {
         cancelOrder(input: $input) {
         cancelOrder(input: $input) {
-            id
-            lines {
-                quantity
-                items {
-                    id
-                    cancelled
-                }
+            ...CanceledOrder
+            ... on ErrorResult {
+                code
+                message
+            }
+        }
+    }
+    fragment CanceledOrder on Order {
+        id
+        lines {
+            quantity
+            items {
+                id
+                cancelled
             }
             }
         }
         }
     }
     }
 `;
 `;
 
 
+const REFUND_FRAGMENT = gql`
+    fragment Refund on Refund {
+        id
+        state
+        items
+        transactionId
+        shipping
+        total
+        metadata
+    }
+`;
+
 export const REFUND_ORDER = gql`
 export const REFUND_ORDER = gql`
     mutation RefundOrder($input: RefundOrderInput!) {
     mutation RefundOrder($input: RefundOrderInput!) {
         refundOrder(input: $input) {
         refundOrder(input: $input) {
-            id
-            state
-            items
-            transactionId
-            shipping
-            total
-            metadata
+            ...Refund
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
+    ${REFUND_FRAGMENT}
 `;
 `;
 
 
 export const SETTLE_REFUND = gql`
 export const SETTLE_REFUND = gql`
     mutation SettleRefund($input: SettleRefundInput!) {
     mutation SettleRefund($input: SettleRefundInput!) {
         settleRefund(input: $input) {
         settleRefund(input: $input) {
-            id
-            state
-            items
-            transactionId
-            shipping
-            total
-            metadata
+            ...Refund
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
+    ${REFUND_FRAGMENT}
 `;
 `;
 
 
 export const GET_ORDER_HISTORY = gql`
 export const GET_ORDER_HISTORY = gql`

+ 49 - 32
packages/core/e2e/product.e2e-spec.ts

@@ -1,7 +1,7 @@
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
@@ -15,6 +15,7 @@ import {
     DeleteProduct,
     DeleteProduct,
     DeleteProductVariant,
     DeleteProductVariant,
     DeletionResult,
     DeletionResult,
+    ErrorCode,
     GetAssetList,
     GetAssetList,
     GetOptionGroup,
     GetOptionGroup,
     GetProductList,
     GetProductList,
@@ -23,6 +24,7 @@ import {
     GetProductWithVariants,
     GetProductWithVariants,
     LanguageCode,
     LanguageCode,
     ProductVariantFragment,
     ProductVariantFragment,
+    ProductWithOptionsFragment,
     ProductWithVariants,
     ProductWithVariants,
     RemoveOptionGroupFromProduct,
     RemoveOptionGroupFromProduct,
     SortOrder,
     SortOrder,
@@ -49,6 +51,10 @@ import { sortById } from './utils/test-order-utils';
 describe('Product resolver', () => {
 describe('Product resolver', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
     const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
 
 
+    const removeOptionGuard: ErrorResultGuard<ProductWithOptionsFragment> = createErrorResultGuard<
+        ProductWithOptionsFragment
+    >(input => !!input.optionGroups);
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -682,31 +688,36 @@ describe('Product resolver', () => {
             });
             });
             expect(addOptionGroupToProduct.optionGroups.length).toBe(1);
             expect(addOptionGroupToProduct.optionGroups.length).toBe(1);
 
 
-            const result = await adminClient.query<
+            const { removeOptionGroupFromProduct } = await adminClient.query<
                 RemoveOptionGroupFromProduct.Mutation,
                 RemoveOptionGroupFromProduct.Mutation,
                 RemoveOptionGroupFromProduct.Variables
                 RemoveOptionGroupFromProduct.Variables
             >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
             >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
                 optionGroupId: 'T_1',
                 optionGroupId: 'T_1',
                 productId: newProductWithAssets.id,
                 productId: newProductWithAssets.id,
             });
             });
-            expect(result.removeOptionGroupFromProduct.id).toBe(newProductWithAssets.id);
-            expect(result.removeOptionGroupFromProduct.optionGroups.length).toBe(0);
+            removeOptionGuard.assertSuccess(removeOptionGroupFromProduct);
+
+            expect(removeOptionGroupFromProduct.id).toBe(newProductWithAssets.id);
+            expect(removeOptionGroupFromProduct.optionGroups.length).toBe(0);
         });
         });
 
 
-        it(
-            'removeOptionGroupFromProduct errors if the optionGroup is being used by variants',
-            assertThrowsWithMessage(
-                () =>
-                    adminClient.query<
-                        RemoveOptionGroupFromProduct.Mutation,
-                        RemoveOptionGroupFromProduct.Variables
-                    >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
-                        optionGroupId: 'T_3',
-                        productId: 'T_2',
-                    }),
+        it('removeOptionGroupFromProduct return error result if the optionGroup is being used by variants', async () => {
+            const { removeOptionGroupFromProduct } = await adminClient.query<
+                RemoveOptionGroupFromProduct.Mutation,
+                RemoveOptionGroupFromProduct.Variables
+            >(REMOVE_OPTION_GROUP_FROM_PRODUCT, {
+                optionGroupId: 'T_3',
+                productId: 'T_2',
+            });
+            removeOptionGuard.assertErrorResult(removeOptionGroupFromProduct);
+
+            expect(removeOptionGroupFromProduct.message).toBe(
                 `Cannot remove ProductOptionGroup "curvy-monitor-monitor-size" as it is used by 2 ProductVariants`,
                 `Cannot remove ProductOptionGroup "curvy-monitor-monitor-size" as it is used by 2 ProductVariants`,
-            ),
-        );
+            );
+            expect(removeOptionGroupFromProduct.code).toBe(ErrorCode.PRODUCT_OPTION_IN_USE_ERROR);
+            expect(removeOptionGroupFromProduct.optionGroupCode).toBe('curvy-monitor-monitor-size');
+            expect(removeOptionGroupFromProduct.productVariantCount).toBe(2);
+        });
 
 
         it(
         it(
             'removeOptionGroupFromProduct errors with an invalid productId',
             'removeOptionGroupFromProduct errors with an invalid productId',
@@ -1157,36 +1168,42 @@ describe('Product resolver', () => {
     });
     });
 });
 });
 
 
-export const ADD_OPTION_GROUP_TO_PRODUCT = gql`
-    mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
-        addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
+const PRODUCT_WITH_OPTIONS_FRAGMENT = gql`
+    fragment ProductWithOptions on Product {
+        id
+        optionGroups {
             id
             id
-            optionGroups {
+            code
+            options {
                 id
                 id
                 code
                 code
-                options {
-                    id
-                    code
-                }
             }
             }
         }
         }
     }
     }
 `;
 `;
 
 
+export const ADD_OPTION_GROUP_TO_PRODUCT = gql`
+    mutation AddOptionGroupToProduct($productId: ID!, $optionGroupId: ID!) {
+        addOptionGroupToProduct(productId: $productId, optionGroupId: $optionGroupId) {
+            ...ProductWithOptions
+        }
+    }
+    ${PRODUCT_WITH_OPTIONS_FRAGMENT}
+`;
+
 export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
 export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
     mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
     mutation RemoveOptionGroupFromProduct($productId: ID!, $optionGroupId: ID!) {
         removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
         removeOptionGroupFromProduct(productId: $productId, optionGroupId: $optionGroupId) {
-            id
-            optionGroups {
-                id
+            ...ProductWithOptions
+            ... on ProductOptionInUseError {
                 code
                 code
-                options {
-                    id
-                    code
-                }
+                message
+                optionGroupCode
+                productVariantCount
             }
             }
         }
         }
     }
     }
+    ${PRODUCT_WITH_OPTIONS_FRAGMENT}
 `;
 `;
 
 
 export const GET_OPTION_GROUP = gql`
 export const GET_OPTION_GROUP = gql`

+ 113 - 87
packages/core/e2e/promotion.e2e-spec.ts

@@ -1,22 +1,24 @@
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { PromotionAction, PromotionCondition, PromotionOrderAction } from '@vendure/core';
 import { PromotionAction, PromotionCondition, PromotionOrderAction } from '@vendure/core';
-import { createTestEnvironment } from '@vendure/testing';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 import path from 'path';
 import path from 'path';
 
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 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 { PROMOTION_FRAGMENT } from './graphql/fragments';
 import { PROMOTION_FRAGMENT } from './graphql/fragments';
 import {
 import {
     CreatePromotion,
     CreatePromotion,
     DeletePromotion,
     DeletePromotion,
     DeletionResult,
     DeletionResult,
+    ErrorCode,
     GetAdjustmentOperations,
     GetAdjustmentOperations,
     GetPromotion,
     GetPromotion,
     GetPromotionList,
     GetPromotionList,
     LanguageCode,
     LanguageCode,
     Promotion,
     Promotion,
+    PromotionFragment,
     UpdatePromotion,
     UpdatePromotion,
 } from './graphql/generated-e2e-admin-types';
 } from './graphql/generated-e2e-admin-types';
 import { CREATE_PROMOTION } from './graphql/shared-definitions';
 import { CREATE_PROMOTION } from './graphql/shared-definitions';
@@ -48,6 +50,10 @@ describe('Promotion resolver', () => {
     ];
     ];
     let promotion: Promotion.Fragment;
     let promotion: Promotion.Fragment;
 
 
+    const promotionGuard: ErrorResultGuard<PromotionFragment> = createErrorResultGuard<PromotionFragment>(
+        input => !!input.couponCode,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -62,100 +68,116 @@ describe('Promotion resolver', () => {
     });
     });
 
 
     it('createPromotion', async () => {
     it('createPromotion', async () => {
-        const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
-            CREATE_PROMOTION,
-            {
-                input: {
-                    name: 'test promotion',
-                    enabled: true,
-                    couponCode: 'TEST123',
-                    startsAt: new Date('2019-10-30T00:00:00.000Z'),
-                    endsAt: new Date('2019-12-01T00:00:00.000Z'),
-                    conditions: [
-                        {
-                            code: promoCondition.code,
-                            arguments: [{ name: 'arg', value: '500' }],
-                        },
-                    ],
-                    actions: [
-                        {
-                            code: promoAction.code,
-                            arguments: [
-                                {
-                                    name: 'facetValueIds',
-                                    value: '["T_1"]',
-                                },
-                            ],
-                        },
-                    ],
-                },
+        const { createPromotion } = await adminClient.query<
+            CreatePromotion.Mutation,
+            CreatePromotion.Variables
+        >(CREATE_PROMOTION, {
+            input: {
+                name: 'test promotion',
+                enabled: true,
+                couponCode: 'TEST123',
+                startsAt: new Date('2019-10-30T00:00:00.000Z'),
+                endsAt: new Date('2019-12-01T00:00:00.000Z'),
+                conditions: [
+                    {
+                        code: promoCondition.code,
+                        arguments: [{ name: 'arg', value: '500' }],
+                    },
+                ],
+                actions: [
+                    {
+                        code: promoAction.code,
+                        arguments: [
+                            {
+                                name: 'facetValueIds',
+                                value: '["T_1"]',
+                            },
+                        ],
+                    },
+                ],
             },
             },
-        );
-        promotion = result.createPromotion;
+        });
+        promotionGuard.assertSuccess(createPromotion);
+
+        promotion = createPromotion;
         expect(pick(promotion, snapshotProps)).toMatchSnapshot();
         expect(pick(promotion, snapshotProps)).toMatchSnapshot();
     });
     });
 
 
-    it(
-        'createPromotion throws with empty conditions and no couponCode',
-        assertThrowsWithMessage(async () => {
-            await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(CREATE_PROMOTION, {
-                input: {
-                    name: 'bad promotion',
-                    enabled: true,
-                    conditions: [],
-                    actions: [
-                        {
-                            code: promoAction.code,
-                            arguments: [
-                                {
-                                    name: 'facetValueIds',
-                                    value: '["T_1"]',
-                                },
-                            ],
-                        },
-                    ],
-                },
-            });
-        }, 'A Promotion must have either at least one condition or a coupon code set'),
-    );
+    it('createPromotion return error result with empty conditions and no couponCode', async () => {
+        const { createPromotion } = await adminClient.query<
+            CreatePromotion.Mutation,
+            CreatePromotion.Variables
+        >(CREATE_PROMOTION, {
+            input: {
+                name: 'bad promotion',
+                enabled: true,
+                conditions: [],
+                actions: [
+                    {
+                        code: promoAction.code,
+                        arguments: [
+                            {
+                                name: 'facetValueIds',
+                                value: '["T_1"]',
+                            },
+                        ],
+                    },
+                ],
+            },
+        });
+        promotionGuard.assertErrorResult(createPromotion);
+
+        expect(createPromotion.message).toBe(
+            'A Promotion must have either at least one condition or a coupon code set',
+        );
+        expect(createPromotion.code).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
+    });
 
 
     it('updatePromotion', async () => {
     it('updatePromotion', async () => {
-        const result = await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(
-            UPDATE_PROMOTION,
-            {
-                input: {
-                    id: promotion.id,
-                    couponCode: 'TEST1235',
-                    startsAt: new Date('2019-05-30T22:00:00.000Z'),
-                    endsAt: new Date('2019-06-01T22:00:00.000Z'),
-                    conditions: [
-                        {
-                            code: promoCondition.code,
-                            arguments: [{ name: 'arg', value: '90' }],
-                        },
-                        {
-                            code: promoCondition2.code,
-                            arguments: [{ name: 'arg', value: '10' }],
-                        },
-                    ],
-                },
+        const { updatePromotion } = await adminClient.query<
+            UpdatePromotion.Mutation,
+            UpdatePromotion.Variables
+        >(UPDATE_PROMOTION, {
+            input: {
+                id: promotion.id,
+                couponCode: 'TEST1235',
+                startsAt: new Date('2019-05-30T22:00:00.000Z'),
+                endsAt: new Date('2019-06-01T22:00:00.000Z'),
+                conditions: [
+                    {
+                        code: promoCondition.code,
+                        arguments: [{ name: 'arg', value: '90' }],
+                    },
+                    {
+                        code: promoCondition2.code,
+                        arguments: [{ name: 'arg', value: '10' }],
+                    },
+                ],
             },
             },
-        );
-        expect(pick(result.updatePromotion, snapshotProps)).toMatchSnapshot();
+        });
+        promotionGuard.assertSuccess(updatePromotion);
+
+        expect(pick(updatePromotion, snapshotProps)).toMatchSnapshot();
     });
     });
 
 
-    it(
-        'updatePromotion throws with empty conditions and no couponCode',
-        assertThrowsWithMessage(async () => {
-            await adminClient.query<UpdatePromotion.Mutation, UpdatePromotion.Variables>(UPDATE_PROMOTION, {
-                input: {
-                    id: promotion.id,
-                    couponCode: '',
-                    conditions: [],
-                },
-            });
-        }, 'A Promotion must have either at least one condition or a coupon code set'),
-    );
+    it('updatePromotion return error result with empty conditions and no couponCode', async () => {
+        const { updatePromotion } = await adminClient.query<
+            UpdatePromotion.Mutation,
+            UpdatePromotion.Variables
+        >(UPDATE_PROMOTION, {
+            input: {
+                id: promotion.id,
+                couponCode: '',
+                conditions: [],
+            },
+        });
+        promotionGuard.assertErrorResult(updatePromotion);
+
+        expect(updatePromotion.message).toBe(
+            'A Promotion must have either at least one condition or a coupon code set',
+        );
+        expect(updatePromotion.code).toBe(ErrorCode.MISSING_CONDITIONS_ERROR);
+    });
 
 
     it('promotion', async () => {
     it('promotion', async () => {
         const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
         const result = await adminClient.query<GetPromotion.Query, GetPromotion.Variables>(GET_PROMOTION, {
@@ -291,6 +313,10 @@ export const UPDATE_PROMOTION = gql`
     mutation UpdatePromotion($input: UpdatePromotionInput!) {
     mutation UpdatePromotion($input: UpdatePromotionInput!) {
         updatePromotion(input: $input) {
         updatePromotion(input: $input) {
             ...Promotion
             ...Promotion
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
     ${PROMOTION_FRAGMENT}
     ${PROMOTION_FRAGMENT}

+ 3 - 1
packages/core/e2e/session-management.e2e-spec.ts

@@ -104,7 +104,9 @@ describe('Session caching', () => {
         await adminClient.query(
         await adminClient.query(
             gql`
             gql`
                 mutation Logout {
                 mutation Logout {
-                    logout
+                    logout {
+                        success
+                    }
                 }
                 }
             `,
             `,
         );
         );

+ 9 - 19
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -245,12 +245,8 @@ describe('Shop auth & accounts', () => {
         });
         });
 
 
         it('login fails before verification', async () => {
         it('login fails before verification', async () => {
-            try {
-                await shopClient.asUserWithCredentials(emailAddress, '');
-                fail('should have thrown');
-            } catch (err) {
-                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
-            }
+            const result = await shopClient.asUserWithCredentials(emailAddress, '');
+            expect(result.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
         });
         });
 
 
         it('verification fails with wrong token', async () => {
         it('verification fails with wrong token', async () => {
@@ -508,7 +504,7 @@ describe('Shop auth & accounts', () => {
             expect(resetPassword.identifier).toBe(customer.emailAddress);
             expect(resetPassword.identifier).toBe(customer.emailAddress);
 
 
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'newPassword');
-            expect(loginResult.user.identifier).toBe(customer.emailAddress);
+            expect(loginResult.identifier).toBe(customer.emailAddress);
         });
         });
 
 
         it('customer history for password reset', async () => {
         it('customer history for password reset', async () => {
@@ -624,12 +620,9 @@ describe('Shop auth & accounts', () => {
         });
         });
 
 
         it('cannot login with new email address before verification', async () => {
         it('cannot login with new email address before verification', async () => {
-            try {
-                await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
-                fail('should have thrown');
-            } catch (err) {
-                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
-            }
+            const result = await shopClient.asUserWithCredentials(NEW_EMAIL_ADDRESS, PASSWORD);
+
+            expect(result.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
         });
         });
 
 
         it('return error result for bad token', async () => {
         it('return error result for bad token', async () => {
@@ -664,12 +657,9 @@ describe('Shop auth & accounts', () => {
         });
         });
 
 
         it('cannot login with old email address after verification', async () => {
         it('cannot login with old email address after verification', async () => {
-            try {
-                await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
-                fail('should have thrown');
-            } catch (err) {
-                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
-            }
+            const result = await shopClient.asUserWithCredentials(customer.emailAddress, PASSWORD);
+
+            expect(result.code).toBe(ErrorCode.INVALID_CREDENTIALS_ERROR);
         });
         });
 
 
         it('customer history for email update', async () => {
         it('customer history for email update', async () => {

+ 1 - 1
packages/core/e2e/shop-customer.e2e-spec.ts

@@ -316,7 +316,7 @@ describe('Shop customers', () => {
 
 
             // Log out and log in with new password
             // Log out and log in with new password
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'test2');
             const loginResult = await shopClient.asUserWithCredentials(customer.emailAddress, 'test2');
-            expect(loginResult.user.identifier).toBe(customer.emailAddress);
+            expect(loginResult.identifier).toBe(customer.emailAddress);
         });
         });
 
 
         it('customer history for CUSTOMER_PASSWORD_UPDATED', async () => {
         it('customer history for CUSTOMER_PASSWORD_UPDATED', async () => {

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

@@ -34,7 +34,7 @@ export class TranslateErrorResultInterceptor implements NestInterceptor {
 
 
     private translateResult(req: any, result: unknown) {
     private translateResult(req: any, result: unknown) {
         if (result instanceof AdminErrorResult || result instanceof ShopErrorResult) {
         if (result instanceof AdminErrorResult || result instanceof ShopErrorResult) {
-            this.i18nService.translateErrorResult(req, result);
+            this.i18nService.translateErrorResult(req, result as any);
         }
         }
     }
     }
 }
 }

+ 19 - 8
packages/core/src/api/resolvers/admin/auth.resolver.ts

@@ -1,12 +1,15 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
-    LoginResult,
+    AuthenticationResult,
     MutationAuthenticateArgs,
     MutationAuthenticateArgs,
     MutationLoginArgs,
     MutationLoginArgs,
+    NativeAuthenticationResult,
     Permission,
     Permission,
+    Success,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { Request, Response } from 'express';
 import { Request, Response } from 'express';
 
 
+import { NativeAuthStrategyError } from '../../../common/error/generated-graphql-admin-errors';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { AdministratorService } from '../../../service/services/administrator.service';
 import { AdministratorService } from '../../../service/services/administrator.service';
 import { AuthService } from '../../../service/services/auth.service';
 import { AuthService } from '../../../service/services/auth.service';
@@ -33,25 +36,29 @@ export class AuthResolver extends BaseAuthResolver {
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    login(
+    async login(
         @Args() args: MutationLoginArgs,
         @Args() args: MutationLoginArgs,
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<LoginResult> {
-        return super.login(args, ctx, req, res);
+    ): Promise<NativeAuthenticationResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        return (await super.login(args, ctx, req, res)) as AuthenticationResult;
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    authenticate(
+    async authenticate(
         @Args() args: MutationAuthenticateArgs,
         @Args() args: MutationAuthenticateArgs,
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<LoginResult> {
-        return this.authenticateAndCreateSession(ctx, args, req, res);
+    ): Promise<AuthenticationResult> {
+        return (await this.authenticateAndCreateSession(ctx, args, req, res)) as AuthenticationResult;
     }
     }
 
 
     @Transaction()
     @Transaction()
@@ -61,7 +68,7 @@ export class AuthResolver extends BaseAuthResolver {
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<boolean> {
+    ): Promise<Success> {
         return super.logout(ctx, req, res);
         return super.logout(ctx, req, res);
     }
     }
 
 
@@ -70,4 +77,8 @@ export class AuthResolver extends BaseAuthResolver {
     me(@Ctx() ctx: RequestContext) {
     me(@Ctx() ctx: RequestContext) {
         return super.me(ctx, 'admin');
         return super.me(ctx, 'admin');
     }
     }
+
+    protected requireNativeAuthStrategy() {
+        return super.requireNativeAuthStrategy() as NativeAuthStrategyError | undefined;
+    }
 }
 }

+ 17 - 7
packages/core/src/api/resolvers/admin/channel.resolver.ts

@@ -1,13 +1,16 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
+    CreateChannelResult,
     DeletionResponse,
     DeletionResponse,
     MutationCreateChannelArgs,
     MutationCreateChannelArgs,
     MutationDeleteChannelArgs,
     MutationDeleteChannelArgs,
     MutationUpdateChannelArgs,
     MutationUpdateChannelArgs,
     Permission,
     Permission,
     QueryChannelArgs,
     QueryChannelArgs,
+    UpdateChannelResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 
 
+import { ErrorResultUnion, isGraphQlErrorResult } from '../../../common/error/error-result';
 import { Channel } from '../../../entity/channel/channel.entity';
 import { Channel } from '../../../entity/channel/channel.entity';
 import { ChannelService } from '../../../service/services/channel.service';
 import { ChannelService } from '../../../service/services/channel.service';
 import { RoleService } from '../../../service/services/role.service';
 import { RoleService } from '../../../service/services/role.service';
@@ -44,13 +47,16 @@ export class ChannelResolver {
     async createChannel(
     async createChannel(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationCreateChannelArgs,
         @Args() args: MutationCreateChannelArgs,
-    ): Promise<Channel> {
-        const channel = await this.channelService.create(ctx, args.input);
+    ): Promise<ErrorResultUnion<CreateChannelResult, Channel>> {
+        const result = await this.channelService.create(ctx, args.input);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
         const superAdminRole = await this.roleService.getSuperAdminRole();
         const superAdminRole = await this.roleService.getSuperAdminRole();
         const customerRole = await this.roleService.getCustomerRole();
         const customerRole = await this.roleService.getCustomerRole();
-        await this.roleService.assignRoleToChannel(ctx, superAdminRole.id, channel.id);
-        await this.roleService.assignRoleToChannel(ctx, customerRole.id, channel.id);
-        return channel;
+        await this.roleService.assignRoleToChannel(ctx, superAdminRole.id, result.id);
+        await this.roleService.assignRoleToChannel(ctx, customerRole.id, result.id);
+        return result;
     }
     }
 
 
     @Transaction()
     @Transaction()
@@ -59,8 +65,12 @@ export class ChannelResolver {
     async updateChannel(
     async updateChannel(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdateChannelArgs,
         @Args() args: MutationUpdateChannelArgs,
-    ): Promise<Channel> {
-        return this.channelService.update(ctx, args.input);
+    ): Promise<ErrorResultUnion<UpdateChannelResult, Channel>> {
+        const result = await this.channelService.update(ctx, args.input);
+        if (isGraphQlErrorResult(result)) {
+            return result;
+        }
+        return result;
     }
     }
 
 
     @Transaction()
     @Transaction()

+ 5 - 3
packages/core/src/api/resolvers/admin/customer.resolver.ts

@@ -1,8 +1,8 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
+    CreateCustomerResult,
     DeletionResponse,
     DeletionResponse,
     MutationAddNoteToCustomerArgs,
     MutationAddNoteToCustomerArgs,
-    MutationAddNoteToOrderArgs,
     MutationCreateCustomerAddressArgs,
     MutationCreateCustomerAddressArgs,
     MutationCreateCustomerArgs,
     MutationCreateCustomerArgs,
     MutationDeleteCustomerAddressArgs,
     MutationDeleteCustomerAddressArgs,
@@ -15,9 +15,11 @@ import {
     QueryCustomerArgs,
     QueryCustomerArgs,
     QueryCustomersArgs,
     QueryCustomersArgs,
     Success,
     Success,
+    UpdateCustomerResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
+import { ErrorResultUnion } from '../../../common/error/error-result';
 import { Address } from '../../../entity/address/address.entity';
 import { Address } from '../../../entity/address/address.entity';
 import { Customer } from '../../../entity/customer/customer.entity';
 import { Customer } from '../../../entity/customer/customer.entity';
 import { CustomerService } from '../../../service/services/customer.service';
 import { CustomerService } from '../../../service/services/customer.service';
@@ -55,7 +57,7 @@ export class CustomerResolver {
     async createCustomer(
     async createCustomer(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationCreateCustomerArgs,
         @Args() args: MutationCreateCustomerArgs,
-    ): Promise<Customer> {
+    ): Promise<ErrorResultUnion<CreateCustomerResult, Customer>> {
         const { input, password } = args;
         const { input, password } = args;
         return this.customerService.create(ctx, input, password || undefined);
         return this.customerService.create(ctx, input, password || undefined);
     }
     }
@@ -66,7 +68,7 @@ export class CustomerResolver {
     async updateCustomer(
     async updateCustomer(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdateCustomerArgs,
         @Args() args: MutationUpdateCustomerArgs,
-    ): Promise<Customer> {
+    ): Promise<ErrorResultUnion<UpdateCustomerResult, Customer>> {
         const { input } = args;
         const { input } = args;
         return this.customerService.update(ctx, input);
         return this.customerService.update(ctx, input);
     }
     }

+ 12 - 5
packages/core/src/api/resolvers/admin/global-settings.resolver.ts

@@ -3,11 +3,15 @@ import {
     MutationUpdateGlobalSettingsArgs,
     MutationUpdateGlobalSettingsArgs,
     OrderProcessState,
     OrderProcessState,
     Permission,
     Permission,
+    UpdateGlobalSettingsResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 
 
+import { ErrorResultUnion } from '../../../common/error/error-result';
 import { UserInputError } from '../../../common/error/errors';
 import { UserInputError } from '../../../common/error/errors';
+import { ChannelDefaultLanguageError } from '../../../common/error/generated-graphql-admin-errors';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { CustomFields } from '../../../config/custom-field/custom-field-types';
 import { CustomFields } from '../../../config/custom-field/custom-field-types';
+import { GlobalSettings } from '../../../entity/global-settings/global-settings.entity';
 import { ChannelService } from '../../../service/services/channel.service';
 import { ChannelService } from '../../../service/services/channel.service';
 import { GlobalSettingsService } from '../../../service/services/global-settings.service';
 import { GlobalSettingsService } from '../../../service/services/global-settings.service';
 import { OrderService } from '../../../service/services/order.service';
 import { OrderService } from '../../../service/services/order.service';
@@ -57,7 +61,10 @@ export class GlobalSettingsResolver {
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateSettings)
     @Allow(Permission.UpdateSettings)
-    async updateGlobalSettings(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateGlobalSettingsArgs) {
+    async updateGlobalSettings(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationUpdateGlobalSettingsArgs,
+    ): Promise<ErrorResultUnion<UpdateGlobalSettingsResult, GlobalSettings>> {
         // This validation is performed here in the resolver rather than at the service
         // This validation is performed here in the resolver rather than at the service
         // layer to avoid a circular dependency [ChannelService <> GlobalSettingsService]
         // layer to avoid a circular dependency [ChannelService <> GlobalSettingsService]
         const { availableLanguages } = args.input;
         const { availableLanguages } = args.input;
@@ -67,10 +74,10 @@ export class GlobalSettingsResolver {
                 c => !availableLanguages.includes(c.defaultLanguageCode),
                 c => !availableLanguages.includes(c.defaultLanguageCode),
             );
             );
             if (unavailableDefaults.length) {
             if (unavailableDefaults.length) {
-                throw new UserInputError('error.cannot-set-default-language-as-unavailable', {
-                    language: unavailableDefaults.map(c => c.defaultLanguageCode).join(', '),
-                    channelCode: unavailableDefaults.map(c => c.code).join(', '),
-                });
+                return new ChannelDefaultLanguageError(
+                    unavailableDefaults.map(c => c.defaultLanguageCode).join(', '),
+                    unavailableDefaults.map(c => c.code).join(', '),
+                );
             }
             }
         }
         }
         return this.globalSettingsService.updateSettings(ctx, args.input);
         return this.globalSettingsService.updateSettings(ctx, args.input);

+ 25 - 5
packages/core/src/api/resolvers/admin/order.resolver.ts

@@ -1,9 +1,11 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
+    AddFulfillmentToOrderResult,
+    CancelOrderResult,
+    MutationAddFulfillmentToOrderArgs,
     MutationAddNoteToOrderArgs,
     MutationAddNoteToOrderArgs,
     MutationCancelOrderArgs,
     MutationCancelOrderArgs,
     MutationDeleteOrderNoteArgs,
     MutationDeleteOrderNoteArgs,
-    MutationFulfillOrderArgs,
     MutationRefundOrderArgs,
     MutationRefundOrderArgs,
     MutationSetOrderCustomFieldsArgs,
     MutationSetOrderCustomFieldsArgs,
     MutationSettlePaymentArgs,
     MutationSettlePaymentArgs,
@@ -14,10 +16,16 @@ import {
     Permission,
     Permission,
     QueryOrderArgs,
     QueryOrderArgs,
     QueryOrdersArgs,
     QueryOrdersArgs,
+    RefundOrderResult,
+    SettlePaymentResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
+import { ErrorResultUnion } from '../../../common/error/error-result';
+import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
+import { Payment } from '../../../entity/payment/payment.entity';
+import { Refund } from '../../../entity/refund/refund.entity';
 import { FulfillmentState } from '../../../service/helpers/fulfillment-state-machine/fulfillment-state';
 import { FulfillmentState } from '../../../service/helpers/fulfillment-state-machine/fulfillment-state';
 import { OrderState } from '../../../service/helpers/order-state-machine/order-state';
 import { OrderState } from '../../../service/helpers/order-state-machine/order-state';
 import { OrderService } from '../../../service/services/order.service';
 import { OrderService } from '../../../service/services/order.service';
@@ -46,28 +54,40 @@ export class OrderResolver {
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     @Allow(Permission.UpdateOrder)
-    async settlePayment(@Ctx() ctx: RequestContext, @Args() args: MutationSettlePaymentArgs) {
+    async settlePayment(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationSettlePaymentArgs,
+    ): Promise<ErrorResultUnion<SettlePaymentResult, Payment>> {
         return this.orderService.settlePayment(ctx, args.id);
         return this.orderService.settlePayment(ctx, args.id);
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     @Allow(Permission.UpdateOrder)
-    async fulfillOrder(@Ctx() ctx: RequestContext, @Args() args: MutationFulfillOrderArgs) {
+    async addFulfillmentToOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationAddFulfillmentToOrderArgs,
+    ): Promise<ErrorResultUnion<AddFulfillmentToOrderResult, Fulfillment>> {
         return this.orderService.createFulfillment(ctx, args.input);
         return this.orderService.createFulfillment(ctx, args.input);
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     @Allow(Permission.UpdateOrder)
-    async cancelOrder(@Ctx() ctx: RequestContext, @Args() args: MutationCancelOrderArgs) {
+    async cancelOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationCancelOrderArgs,
+    ): Promise<ErrorResultUnion<CancelOrderResult, Order>> {
         return this.orderService.cancelOrder(ctx, args.input);
         return this.orderService.cancelOrder(ctx, args.input);
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateOrder)
     @Allow(Permission.UpdateOrder)
-    async refundOrder(@Ctx() ctx: RequestContext, @Args() args: MutationRefundOrderArgs) {
+    async refundOrder(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationRefundOrderArgs,
+    ): Promise<ErrorResultUnion<RefundOrderResult, Refund>> {
         return this.orderService.refundOrder(ctx, args.input);
         return this.orderService.refundOrder(ctx, args.input);
     }
     }
 
 

+ 3 - 1
packages/core/src/api/resolvers/admin/product.resolver.ts

@@ -15,9 +15,11 @@ import {
     QueryProductArgs,
     QueryProductArgs,
     QueryProductsArgs,
     QueryProductsArgs,
     QueryProductVariantArgs,
     QueryProductVariantArgs,
+    RemoveOptionGroupFromProductResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
+import { ErrorResultUnion } from '../../../common/error/error-result';
 import { UserInputError } from '../../../common/error/errors';
 import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
 import { Translated } from '../../../common/types/locale-types';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
@@ -124,7 +126,7 @@ export class ProductResolver {
     async removeOptionGroupFromProduct(
     async removeOptionGroupFromProduct(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationRemoveOptionGroupFromProductArgs,
         @Args() args: MutationRemoveOptionGroupFromProductArgs,
-    ): Promise<Translated<Product>> {
+    ): Promise<ErrorResultUnion<RemoveOptionGroupFromProductResult, Translated<Product>>> {
         const { productId, optionGroupId } = args;
         const { productId, optionGroupId } = args;
         return this.productService.removeOptionGroupFromProduct(ctx, productId, optionGroupId);
         return this.productService.removeOptionGroupFromProduct(ctx, productId, optionGroupId);
     }
     }

+ 14 - 7
packages/core/src/api/resolvers/admin/promotion.resolver.ts

@@ -1,5 +1,6 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
+    CreatePromotionResult,
     DeletionResponse,
     DeletionResponse,
     MutationCreatePromotionArgs,
     MutationCreatePromotionArgs,
     MutationDeletePromotionArgs,
     MutationDeletePromotionArgs,
@@ -7,9 +8,11 @@ import {
     Permission,
     Permission,
     QueryPromotionArgs,
     QueryPromotionArgs,
     QueryPromotionsArgs,
     QueryPromotionsArgs,
+    UpdatePromotionResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
+import { ErrorResultUnion } from '../../../common/error/error-result';
 import { PromotionItemAction, PromotionOrderAction } from '../../../config/promotion/promotion-action';
 import { PromotionItemAction, PromotionOrderAction } from '../../../config/promotion/promotion-action';
 import { PromotionCondition } from '../../../config/promotion/promotion-condition';
 import { PromotionCondition } from '../../../config/promotion/promotion-condition';
 import { Promotion } from '../../../entity/promotion/promotion.entity';
 import { Promotion } from '../../../entity/promotion/promotion.entity';
@@ -63,7 +66,7 @@ export class PromotionResolver {
     createPromotion(
     createPromotion(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationCreatePromotionArgs,
         @Args() args: MutationCreatePromotionArgs,
-    ): Promise<Promotion> {
+    ): Promise<ErrorResultUnion<CreatePromotionResult, Promotion>> {
         this.configurableOperationCodec.decodeConfigurableOperationIds(
         this.configurableOperationCodec.decodeConfigurableOperationIds(
             PromotionOrderAction,
             PromotionOrderAction,
             args.input.actions,
             args.input.actions,
@@ -81,7 +84,7 @@ export class PromotionResolver {
     updatePromotion(
     updatePromotion(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: MutationUpdatePromotionArgs,
         @Args() args: MutationUpdatePromotionArgs,
-    ): Promise<Promotion> {
+    ): Promise<ErrorResultUnion<UpdatePromotionResult, Promotion>> {
         this.configurableOperationCodec.decodeConfigurableOperationIds(
         this.configurableOperationCodec.decodeConfigurableOperationIds(
             PromotionOrderAction,
             PromotionOrderAction,
             args.input.actions || [],
             args.input.actions || [],
@@ -110,17 +113,21 @@ export class PromotionResolver {
     /**
     /**
      * Encodes any entity IDs used in the filter arguments.
      * Encodes any entity IDs used in the filter arguments.
      */
      */
-    private encodeConditionsAndActions = <T extends Promotion | undefined>(collection: T): T => {
-        if (collection) {
+    private encodeConditionsAndActions = <
+        T extends ErrorResultUnion<CreatePromotionResult, Promotion> | undefined
+    >(
+        maybePromotion: T,
+    ): T => {
+        if (maybePromotion instanceof Promotion) {
             this.configurableOperationCodec.encodeConfigurableOperationIds(
             this.configurableOperationCodec.encodeConfigurableOperationIds(
                 PromotionOrderAction,
                 PromotionOrderAction,
-                collection.actions,
+                maybePromotion.actions,
             );
             );
             this.configurableOperationCodec.encodeConfigurableOperationIds(
             this.configurableOperationCodec.encodeConfigurableOperationIds(
                 PromotionCondition,
                 PromotionCondition,
-                collection.conditions,
+                maybePromotion.conditions,
             );
             );
         }
         }
-        return collection;
+        return maybePromotion;
     };
     };
 }
 }

+ 43 - 12
packages/core/src/api/resolvers/base/base-auth.resolver.ts

@@ -1,16 +1,24 @@
+import { AuthenticationResult as ShopAuthenticationResult } from '@vendure/common/lib/generated-shop-types';
 import {
 import {
+    AuthenticationResult as AdminAuthenticationResult,
     CurrentUser,
     CurrentUser,
     CurrentUserChannel,
     CurrentUserChannel,
-    LoginResult,
     MutationAuthenticateArgs,
     MutationAuthenticateArgs,
     MutationLoginArgs,
     MutationLoginArgs,
+    Success,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { Request, Response } from 'express';
 import { Request, Response } from 'express';
 
 
+import { isGraphQlErrorResult } from '../../../common/error/error-result';
 import { ForbiddenError, UnauthorizedError } from '../../../common/error/errors';
 import { ForbiddenError, UnauthorizedError } from '../../../common/error/errors';
-import { InvalidCredentialsError } from '../../../common/error/generated-graphql-shop-errors';
+import { NativeAuthStrategyError as AdminNativeAuthStrategyError } from '../../../common/error/generated-graphql-admin-errors';
+import {
+    InvalidCredentialsError,
+    NativeAuthStrategyError as ShopNativeAuthStrategyError,
+} from '../../../common/error/generated-graphql-shop-errors';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
+import { Logger } from '../../../config/logger/vendure-logger';
 import { User } from '../../../entity/user/user.entity';
 import { User } from '../../../entity/user/user.entity';
 import { getUserChannelsPermissions } from '../../../service/helpers/utils/get-user-channels-permissions';
 import { getUserChannelsPermissions } from '../../../service/helpers/utils/get-user-channels-permissions';
 import { AdministratorService } from '../../../service/services/administrator.service';
 import { AdministratorService } from '../../../service/services/administrator.service';
@@ -22,12 +30,18 @@ import { RequestContext } from '../../common/request-context';
 import { setSessionToken } from '../../common/set-session-token';
 import { setSessionToken } from '../../common/set-session-token';
 
 
 export class BaseAuthResolver {
 export class BaseAuthResolver {
+    protected readonly nativeAuthStrategyIsConfigured: boolean;
+
     constructor(
     constructor(
         protected authService: AuthService,
         protected authService: AuthService,
         protected userService: UserService,
         protected userService: UserService,
         protected administratorService: AdministratorService,
         protected administratorService: AdministratorService,
         protected configService: ConfigService,
         protected configService: ConfigService,
-    ) {}
+    ) {
+        this.nativeAuthStrategyIsConfigured = !!this.configService.authOptions.shopAuthenticationStrategy.find(
+            strategy => strategy.name === NATIVE_AUTH_STRATEGY_NAME,
+        );
+    }
 
 
     /**
     /**
      * Attempts a login given the username and password of a user. If successful, returns
      * Attempts a login given the username and password of a user. If successful, returns
@@ -38,7 +52,7 @@ export class BaseAuthResolver {
         ctx: RequestContext,
         ctx: RequestContext,
         req: Request,
         req: Request,
         res: Response,
         res: Response,
-    ): Promise<LoginResult> {
+    ): Promise<AdminAuthenticationResult | ShopAuthenticationResult> {
         return await this.authenticateAndCreateSession(
         return await this.authenticateAndCreateSession(
             ctx,
             ctx,
             {
             {
@@ -49,10 +63,10 @@ export class BaseAuthResolver {
         );
         );
     }
     }
 
 
-    async logout(ctx: RequestContext, req: Request, res: Response): Promise<boolean> {
+    async logout(ctx: RequestContext, req: Request, res: Response): Promise<Success> {
         const token = extractSessionToken(req, this.configService.authOptions.tokenMethod);
         const token = extractSessionToken(req, this.configService.authOptions.tokenMethod);
         if (!token) {
         if (!token) {
-            return false;
+            return { success: false };
         }
         }
         await this.authService.destroyAuthenticatedSession(ctx, token);
         await this.authService.destroyAuthenticatedSession(ctx, token);
         setSessionToken({
         setSessionToken({
@@ -62,7 +76,7 @@ export class BaseAuthResolver {
             rememberMe: false,
             rememberMe: false,
             sessionToken: '',
             sessionToken: '',
         });
         });
-        return true;
+        return { success: true };
     }
     }
 
 
     /**
     /**
@@ -91,14 +105,17 @@ export class BaseAuthResolver {
         args: MutationAuthenticateArgs,
         args: MutationAuthenticateArgs,
         req: Request,
         req: Request,
         res: Response,
         res: Response,
-    ): Promise<LoginResult> {
+    ): Promise<AdminAuthenticationResult | ShopAuthenticationResult> {
         const [method, data] = Object.entries(args.input)[0];
         const [method, data] = Object.entries(args.input)[0];
         const { apiType } = ctx;
         const { apiType } = ctx;
         const session = await this.authService.authenticate(ctx, apiType, method, data);
         const session = await this.authService.authenticate(ctx, apiType, method, data);
+        if (isGraphQlErrorResult(session)) {
+            return session;
+        }
         if (apiType && apiType === 'admin') {
         if (apiType && apiType === 'admin') {
             const administrator = await this.administratorService.findOneByUserId(ctx, session.user.id);
             const administrator = await this.administratorService.findOneByUserId(ctx, session.user.id);
             if (!administrator) {
             if (!administrator) {
-                throw new UnauthorizedError();
+                return new InvalidCredentialsError();
             }
             }
         }
         }
         setSessionToken({
         setSessionToken({
@@ -108,9 +125,7 @@ export class BaseAuthResolver {
             rememberMe: args.rememberMe || false,
             rememberMe: args.rememberMe || false,
             sessionToken: session.token,
             sessionToken: session.token,
         });
         });
-        return {
-            user: this.publiclyAccessibleUser(session.user),
-        };
+        return this.publiclyAccessibleUser(session.user);
     }
     }
 
 
     /**
     /**
@@ -138,4 +153,20 @@ export class BaseAuthResolver {
             channels: getUserChannelsPermissions(user) as CurrentUserChannel[],
             channels: getUserChannelsPermissions(user) as CurrentUserChannel[],
         };
         };
     }
     }
+
+    protected requireNativeAuthStrategy():
+        | AdminNativeAuthStrategyError
+        | ShopNativeAuthStrategyError
+        | undefined {
+        if (!this.nativeAuthStrategyIsConfigured) {
+            const authStrategyNames = this.configService.authOptions.shopAuthenticationStrategy
+                .map(s => s.name)
+                .join(', ');
+            const errorMessage =
+                'This GraphQL operation requires that the NativeAuthenticationStrategy be configured for the Shop API.\n' +
+                `Currently the following AuthenticationStrategies are enabled: ${authStrategyNames}`;
+            Logger.error(errorMessage);
+            return new AdminNativeAuthStrategyError();
+        }
+    }
 }
 }

+ 31 - 35
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -1,7 +1,9 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
+    AuthenticationResult,
     ErrorCode,
     ErrorCode,
-    LoginResult,
+    InvalidCredentialsError,
+    MissingPasswordError,
     MutationAuthenticateArgs,
     MutationAuthenticateArgs,
     MutationLoginArgs,
     MutationLoginArgs,
     MutationRefreshCustomerVerificationArgs,
     MutationRefreshCustomerVerificationArgs,
@@ -12,12 +14,14 @@ import {
     MutationUpdateCustomerEmailAddressArgs,
     MutationUpdateCustomerEmailAddressArgs,
     MutationUpdateCustomerPasswordArgs,
     MutationUpdateCustomerPasswordArgs,
     MutationVerifyCustomerAccountArgs,
     MutationVerifyCustomerAccountArgs,
+    NativeAuthenticationResult,
     Permission,
     Permission,
     RefreshCustomerVerificationResult,
     RefreshCustomerVerificationResult,
     RegisterCustomerAccountResult,
     RegisterCustomerAccountResult,
     RequestPasswordResetResult,
     RequestPasswordResetResult,
     RequestUpdateCustomerEmailAddressResult,
     RequestUpdateCustomerEmailAddressResult,
     ResetPasswordResult,
     ResetPasswordResult,
+    Success,
     UpdateCustomerEmailAddressResult,
     UpdateCustomerEmailAddressResult,
     UpdateCustomerPasswordResult,
     UpdateCustomerPasswordResult,
     VerifyCustomerAccountResult,
     VerifyCustomerAccountResult,
@@ -30,7 +34,6 @@ import { ForbiddenError } from '../../../common/error/errors';
 import { NativeAuthStrategyError } from '../../../common/error/generated-graphql-shop-errors';
 import { NativeAuthStrategyError } from '../../../common/error/generated-graphql-shop-errors';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { NATIVE_AUTH_STRATEGY_NAME } from '../../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
-import { Logger } from '../../../config/logger/vendure-logger';
 import { AdministratorService } from '../../../service/services/administrator.service';
 import { AdministratorService } from '../../../service/services/administrator.service';
 import { AuthService } from '../../../service/services/auth.service';
 import { AuthService } from '../../../service/services/auth.service';
 import { CustomerService } from '../../../service/services/customer.service';
 import { CustomerService } from '../../../service/services/customer.service';
@@ -45,8 +48,6 @@ import { BaseAuthResolver } from '../base/base-auth.resolver';
 
 
 @Resolver()
 @Resolver()
 export class ShopAuthResolver extends BaseAuthResolver {
 export class ShopAuthResolver extends BaseAuthResolver {
-    private readonly nativeAuthStrategyIsConfigured: boolean;
-
     constructor(
     constructor(
         authService: AuthService,
         authService: AuthService,
         userService: UserService,
         userService: UserService,
@@ -56,44 +57,44 @@ export class ShopAuthResolver extends BaseAuthResolver {
         protected historyService: HistoryService,
         protected historyService: HistoryService,
     ) {
     ) {
         super(authService, userService, administratorService, configService);
         super(authService, userService, administratorService, configService);
-        this.nativeAuthStrategyIsConfigured = !!this.configService.authOptions.shopAuthenticationStrategy.find(
-            strategy => strategy.name === NATIVE_AUTH_STRATEGY_NAME,
-        );
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    login(
+    async login(
         @Args() args: MutationLoginArgs,
         @Args() args: MutationLoginArgs,
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<LoginResult> {
-        this.requireNativeAuthStrategy();
-        return super.login(args, ctx, req, res);
+    ): Promise<NativeAuthenticationResult> {
+        const nativeAuthStrategyError = this.requireNativeAuthStrategy();
+        if (nativeAuthStrategyError) {
+            return nativeAuthStrategyError;
+        }
+        return (await super.login(args, ctx, req, res)) as AuthenticationResult;
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    authenticate(
+    async authenticate(
         @Args() args: MutationAuthenticateArgs,
         @Args() args: MutationAuthenticateArgs,
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<LoginResult> {
-        return this.authenticateAndCreateSession(ctx, args, req, res);
+    ): Promise<AuthenticationResult> {
+        return (await this.authenticateAndCreateSession(ctx, args, req, res)) as AuthenticationResult;
     }
     }
 
 
     @Transaction()
     @Transaction()
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @Allow(Permission.Public)
-    logout(
+    async logout(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Context('req') req: Request,
         @Context('req') req: Request,
         @Context('res') res: Response,
         @Context('res') res: Response,
-    ): Promise<boolean> {
+    ): Promise<Success> {
         return super.logout(ctx, req, res);
         return super.logout(ctx, req, res);
     }
     }
 
 
@@ -121,7 +122,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
                 // otherwise account enumeration attacks become possible.
                 // otherwise account enumeration attacks become possible.
                 return { success: true };
                 return { success: true };
             }
             }
-            return result;
+            return result as MissingPasswordError;
         }
         }
         return { success: true };
         return { success: true };
     }
     }
@@ -210,17 +211,17 @@ export class ShopAuthResolver extends BaseAuthResolver {
             return nativeAuthStrategyError;
             return nativeAuthStrategyError;
         }
         }
         const { token, password } = args;
         const { token, password } = args;
-        const result = await this.customerService.resetPassword(ctx, token, password);
-        if (isGraphQlErrorResult(result)) {
-            return result;
+        const resetResult = await this.customerService.resetPassword(ctx, token, password);
+        if (isGraphQlErrorResult(resetResult)) {
+            return resetResult;
         }
         }
 
 
-        const { user } = await super.authenticateAndCreateSession(
+        const authResult = await super.authenticateAndCreateSession(
             ctx,
             ctx,
             {
             {
                 input: {
                 input: {
                     [NATIVE_AUTH_STRATEGY_NAME]: {
                     [NATIVE_AUTH_STRATEGY_NAME]: {
-                        username: result.identifier,
+                        username: resetResult.identifier,
                         password: args.password,
                         password: args.password,
                     },
                     },
                 },
                 },
@@ -228,7 +229,11 @@ export class ShopAuthResolver extends BaseAuthResolver {
             req,
             req,
             res,
             res,
         );
         );
-        return user;
+        if (isGraphQlErrorResult(authResult)) {
+            // This should never occur in theory
+            throw authResult;
+        }
+        return authResult;
     }
     }
 
 
     @Transaction()
     @Transaction()
@@ -276,7 +281,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         }
         }
         const verify = await this.authService.verifyUserPassword(ctx, ctx.activeUserId, args.password);
         const verify = await this.authService.verifyUserPassword(ctx, ctx.activeUserId, args.password);
         if (isGraphQlErrorResult(verify)) {
         if (isGraphQlErrorResult(verify)) {
-            return verify;
+            return verify as InvalidCredentialsError;
         }
         }
         const result = await this.customerService.requestUpdateEmailAddress(
         const result = await this.customerService.requestUpdateEmailAddress(
             ctx,
             ctx,
@@ -309,16 +314,7 @@ export class ShopAuthResolver extends BaseAuthResolver {
         return { success: result };
         return { success: result };
     }
     }
 
 
-    private requireNativeAuthStrategy(): NativeAuthStrategyError | undefined {
-        if (!this.nativeAuthStrategyIsConfigured) {
-            const authStrategyNames = this.configService.authOptions.shopAuthenticationStrategy
-                .map(s => s.name)
-                .join(', ');
-            const errorMessage =
-                'This GraphQL operation requires that the NativeAuthenticationStrategy be configured for the Shop API.\n' +
-                `Currently the following AuthenticationStrategies are enabled: ${authStrategyNames}`;
-            Logger.error(errorMessage);
-            return new NativeAuthStrategyError();
-        }
+    protected requireNativeAuthStrategy() {
+        return super.requireNativeAuthStrategy() as NativeAuthStrategyError | undefined;
     }
     }
 }
 }

+ 5 - 2
packages/core/src/api/resolvers/shop/shop-customer.resolver.ts

@@ -1,9 +1,12 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
-import { MutationDeleteCustomerAddressArgs, Success } from '@vendure/common/lib/generated-shop-types';
+import {
+    MutationDeleteCustomerAddressArgs,
+    MutationUpdateCustomerArgs,
+    Success,
+} from '@vendure/common/lib/generated-shop-types';
 import {
 import {
     MutationCreateCustomerAddressArgs,
     MutationCreateCustomerAddressArgs,
     MutationUpdateCustomerAddressArgs,
     MutationUpdateCustomerAddressArgs,
-    MutationUpdateCustomerArgs,
     Permission,
     Permission,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 
 

+ 6 - 3
packages/core/src/api/schema/admin-api/auth.api.graphql

@@ -4,11 +4,14 @@ type Query {
 
 
 type Mutation {
 type Mutation {
     "Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})`"
     "Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})`"
-    login(username: String!, password: String!, rememberMe: Boolean): LoginResult!
+    login(username: String!, password: String!, rememberMe: Boolean): NativeAuthenticationResult!
     "Authenticates the user using a named authentication strategy"
     "Authenticates the user using a named authentication strategy"
-    authenticate(input: AuthenticationInput!, rememberMe: Boolean): LoginResult!
-    logout: Boolean!
+    authenticate(input: AuthenticationInput!, rememberMe: Boolean): AuthenticationResult!
+    logout: Success!
 }
 }
 
 
 # Populated at run-time
 # Populated at run-time
 input AuthenticationInput
 input AuthenticationInput
+
+union NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError
+union AuthenticationResult = CurrentUser | InvalidCredentialsError

+ 12 - 2
packages/core/src/api/schema/admin-api/channel.api.graphql

@@ -6,10 +6,10 @@ type Query {
 
 
 type Mutation {
 type Mutation {
     "Create a new Channel"
     "Create a new Channel"
-    createChannel(input: CreateChannelInput!): Channel!
+    createChannel(input: CreateChannelInput!): CreateChannelResult!
 
 
     "Update an existing Channel"
     "Update an existing Channel"
-    updateChannel(input: UpdateChannelInput!): Channel!
+    updateChannel(input: UpdateChannelInput!): UpdateChannelResult!
 
 
     "Delete a Channel"
     "Delete a Channel"
     deleteChannel(id: ID!): DeletionResponse!
     deleteChannel(id: ID!): DeletionResponse!
@@ -35,3 +35,13 @@ input UpdateChannelInput {
     defaultTaxZoneId: ID
     defaultTaxZoneId: ID
     defaultShippingZoneId: ID
     defaultShippingZoneId: ID
 }
 }
+
+"Returned if attempting to set a Channel's defaultLanguageCode to a language which is not enabled in GlobalSettings"
+type LanguageNotAvailableError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    languageCode: String!
+}
+
+union CreateChannelResult = Channel | LanguageNotAvailableError
+union UpdateChannelResult = Channel | LanguageNotAvailableError

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

@@ -5,10 +5,10 @@ type Query {
 
 
 type Mutation {
 type Mutation {
     "Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer."
     "Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer."
-    createCustomer(input: CreateCustomerInput!, password: String): Customer!
+    createCustomer(input: CreateCustomerInput!, password: String): CreateCustomerResult!
 
 
     "Update an existing Customer"
     "Update an existing Customer"
-    updateCustomer(input: UpdateCustomerInput!): Customer!
+    updateCustomer(input: UpdateCustomerInput!): UpdateCustomerResult!
 
 
     "Delete a Customer"
     "Delete a Customer"
     deleteCustomer(id: ID!): DeletionResponse!
     deleteCustomer(id: ID!): DeletionResponse!
@@ -56,3 +56,6 @@ input UpdateCustomerNoteInput {
     note: String!
     note: String!
 }
 }
 
 
+union CreateCustomerResult = Customer | EmailAddressConflictError
+union UpdateCustomerResult = Customer | EmailAddressConflictError
+

+ 14 - 1
packages/core/src/api/schema/admin-api/global-settings.api.graphql

@@ -3,10 +3,23 @@ type Query {
 }
 }
 
 
 type Mutation {
 type Mutation {
-    updateGlobalSettings(input: UpdateGlobalSettingsInput!): GlobalSettings!
+    updateGlobalSettings(input: UpdateGlobalSettingsInput!): UpdateGlobalSettingsResult!
 }
 }
 
 
 input UpdateGlobalSettingsInput {
 input UpdateGlobalSettingsInput {
     availableLanguages: [LanguageCode!]
     availableLanguages: [LanguageCode!]
     trackInventory: Boolean
     trackInventory: Boolean
 }
 }
+
+"""
+Returned when the default LanguageCode of a Channel is no longer found in the `availableLanguages`
+of the GlobalSettings
+"""
+type ChannelDefaultLanguageError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    language: String!
+    channelCode: String!
+}
+
+union UpdateGlobalSettingsResult = GlobalSettings | ChannelDefaultLanguageError

+ 118 - 6
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -4,16 +4,16 @@ type Query {
 }
 }
 
 
 type Mutation {
 type Mutation {
-    settlePayment(id: ID!): Payment!
-    fulfillOrder(input: FulfillOrderInput!): Fulfillment!
-    cancelOrder(input: CancelOrderInput!): Order!
-    refundOrder(input: RefundOrderInput!): Refund!
-    settleRefund(input: SettleRefundInput!): Refund!
+    settlePayment(id: ID!): SettlePaymentResult!
+    addFulfillmentToOrder(input: FulfillOrderInput!): AddFulfillmentToOrderResult!
+    cancelOrder(input: CancelOrderInput!): CancelOrderResult!
+    refundOrder(input: RefundOrderInput!): RefundOrderResult!
+    settleRefund(input: SettleRefundInput!): SettleRefundResult!
     addNoteToOrder(input: AddNoteToOrderInput!): Order!
     addNoteToOrder(input: AddNoteToOrderInput!): Order!
     updateOrderNote(input: UpdateOrderNoteInput!): HistoryEntry!
     updateOrderNote(input: UpdateOrderNoteInput!): HistoryEntry!
     deleteOrderNote(id: ID!): DeletionResponse!
     deleteOrderNote(id: ID!): DeletionResponse!
     transitionOrderToState(id: ID!, state: String!): TransitionOrderToStateResult
     transitionOrderToState(id: ID!, state: String!): TransitionOrderToStateResult
-    transitionFulfillmentToState(id: ID!, state: String!): Fulfillment!
+    transitionFulfillmentToState(id: ID!, state: String!): TransitionFulfillmentToStateResult!
     setOrderCustomFields(input: UpdateOrderInput!): Order
     setOrderCustomFields(input: UpdateOrderInput!): Order
 }
 }
 
 
@@ -73,4 +73,116 @@ input UpdateOrderNoteInput {
     isPublic: Boolean
     isPublic: Boolean
 }
 }
 
 
+"Returned if the Payment settlement fails"
+type SettlePaymentError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    paymentErrorMessage: String!
+}
+
+"Returned if no OrderLines have been specified for the operation"
+type EmptyOrderLineSelectionError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if the specified items are already part of a Fulfillment"
+type ItemsAlreadyFulfilledError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if an operation has specified OrderLines from multiple Orders"
+type MultipleOrderError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if an attempting to cancel lines from an Order which is still active"
+type CancelActiveOrderError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    orderState: String!
+}
+
+"Returned if an attempting to refund a Payment against OrderLines from a different Order"
+type PaymentOrderMismatchError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if an attempting to refund an Order which is not in the expected state"
+type RefundOrderStateError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    orderState: String!
+}
+
+"Returned if an attempting to refund an Order but neither items nor shipping refund was specified"
+type NothingToRefundError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if an attempting to refund an OrderItem which has already been refunded"
+type AlreadyRefundedError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    refundId: ID!
+}
+
+"Returned if the specified quantity of an OrderLine is greater than the number of items in that line"
+type QuantityTooGreatError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned when there is an error in transitioning the Refund state"
+type RefundStateTransitionError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    transitionError: String!
+    fromState: String!
+    toState: String!
+}
+
+"Returned when there is an error in transitioning the Payment state"
+type PaymentStateTransitionError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    transitionError: String!
+    fromState: String!
+    toState: String!
+}
+
+"Returned when there is an error in transitioning the Fulfillment state"
+type FulfillmentStateTransitionError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    transitionError: String!
+    fromState: String!
+    toState: String!
+}
+
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
 union TransitionOrderToStateResult = Order | OrderStateTransitionError
+union SettlePaymentResult = Payment | SettlePaymentError | PaymentStateTransitionError | OrderStateTransitionError
+union AddFulfillmentToOrderResult = Fulfillment | EmptyOrderLineSelectionError | ItemsAlreadyFulfilledError
+union CancelOrderResult =
+      Order
+    | EmptyOrderLineSelectionError
+    | QuantityTooGreatError
+    | MultipleOrderError
+    | CancelActiveOrderError
+    | OrderStateTransitionError
+union RefundOrderResult =
+      Refund
+    | QuantityTooGreatError
+    | NothingToRefundError
+    | OrderStateTransitionError
+    | MultipleOrderError
+    | PaymentOrderMismatchError
+    | RefundOrderStateError
+    | AlreadyRefundedError
+    | RefundStateTransitionError
+union SettleRefundResult = Refund | RefundStateTransitionError
+union TransitionFulfillmentToStateResult = Fulfillment | FulfillmentStateTransitionError

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

@@ -20,7 +20,7 @@ type Mutation {
     addOptionGroupToProduct(productId: ID!, optionGroupId: ID!): Product!
     addOptionGroupToProduct(productId: ID!, optionGroupId: ID!): Product!
 
 
     "Remove an OptionGroup from a Product"
     "Remove an OptionGroup from a Product"
-    removeOptionGroupFromProduct(productId: ID!, optionGroupId: ID!): Product!
+    removeOptionGroupFromProduct(productId: ID!, optionGroupId: ID!): RemoveOptionGroupFromProductResult!
 
 
     "Create a set of ProductVariants based on the OptionGroups assigned to the given Product"
     "Create a set of ProductVariants based on the OptionGroups assigned to the given Product"
     createProductVariants(input: [CreateProductVariantInput!]!): [ProductVariant]!
     createProductVariants(input: [CreateProductVariantInput!]!): [ProductVariant]!
@@ -139,3 +139,12 @@ input RemoveProductsFromChannelInput {
     productIds: [ID!]!
     productIds: [ID!]!
     channelId: ID!
     channelId: ID!
 }
 }
+
+type ProductOptionInUseError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+    optionGroupCode: String!
+    productVariantCount: Int!
+}
+
+union RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError

+ 11 - 2
packages/core/src/api/schema/admin-api/promotion.api.graphql

@@ -6,8 +6,8 @@ type Query {
 }
 }
 
 
 type Mutation {
 type Mutation {
-    createPromotion(input: CreatePromotionInput!): Promotion!
-    updatePromotion(input: UpdatePromotionInput!): Promotion!
+    createPromotion(input: CreatePromotionInput!): CreatePromotionResult!
+    updatePromotion(input: UpdatePromotionInput!): UpdatePromotionResult!
     deletePromotion(id: ID!): DeletionResponse!
     deletePromotion(id: ID!): DeletionResponse!
 }
 }
 
 
@@ -36,3 +36,12 @@ input UpdatePromotionInput {
     conditions: [ConfigurableOperationInput!]
     conditions: [ConfigurableOperationInput!]
     actions: [ConfigurableOperationInput!]
     actions: [ConfigurableOperationInput!]
 }
 }
+
+"Returned if a PromotionCondition has neither a couponCode nor any conditions set"
+type MissingConditionsError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+union CreatePromotionResult = Promotion | MissingConditionsError
+union UpdatePromotionResult = Promotion | MissingConditionsError

+ 18 - 0
packages/core/src/api/schema/common/common-types.graphql

@@ -191,6 +191,18 @@ type Success {
     success: Boolean!
     success: Boolean!
 }
 }
 
 
+"Retured when attempting an operation that relies on the NativeAuthStrategy, if that strategy is not configured."
+type NativeAuthStrategyError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
+"Returned if the user authentication credentials are not valid"
+type InvalidCredentialsError implements ErrorResult {
+    code: ErrorCode!
+    message: String!
+}
+
 "Returned if there is an error in transitioning the Order state"
 "Returned if there is an error in transitioning the Order state"
 type OrderStateTransitionError implements ErrorResult {
 type OrderStateTransitionError implements ErrorResult {
     code: ErrorCode!
     code: ErrorCode!
@@ -199,3 +211,9 @@ type OrderStateTransitionError implements ErrorResult {
     fromState: String!
     fromState: String!
     toState: String!
     toState: 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!
+}

+ 5 - 21
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -69,11 +69,11 @@ type Mutation {
     "Set the Customer for the Order. Required only if the Customer is not currently logged in"
     "Set the Customer for the Order. Required only if the Customer is not currently logged in"
     setCustomerForOrder(input: CreateCustomerInput!): SetCustomerForOrderResult
     setCustomerForOrder(input: CreateCustomerInput!): SetCustomerForOrderResult
     "Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})`"
     "Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})`"
-    login(username: String!, password: String!, rememberMe: Boolean): LoginResult!
+    login(username: String!, password: String!, rememberMe: Boolean): NativeAuthenticationResult!
     "Authenticates the user using a named authentication strategy"
     "Authenticates the user using a named authentication strategy"
-    authenticate(input: AuthenticationInput!, rememberMe: Boolean): LoginResult!
+    authenticate(input: AuthenticationInput!, rememberMe: Boolean): AuthenticationResult!
     "End the current authenticated session"
     "End the current authenticated session"
-    logout: Boolean!
+    logout: Success!
     """
     """
     Register a Customer account with the given credentials. There are three possible registration flows:
     Register a Customer account with the given credentials. There are three possible registration flows:
 
 
@@ -252,12 +252,6 @@ type AlreadyLoggedInError implements ErrorResult {
     message: String!
     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."
 "Retured when attemting to register or verify a customer account without a password, when one is required."
 type MissingPasswordError implements ErrorResult {
 type MissingPasswordError implements ErrorResult {
     code: ErrorCode!
     code: ErrorCode!
@@ -270,12 +264,6 @@ type PasswordAlreadySetError implements ErrorResult {
     message: String!
     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
 Retured if the verification token (used to verify a Customer's email address) is either
 invalid or does not match any expected tokens.
 invalid or does not match any expected tokens.
@@ -312,12 +300,6 @@ type IdentifierChangeTokenExpiredError implements ErrorResult {
     message: String!
     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
 Retured if the token used to reset a Customer's password is either
 invalid or does not match any expected tokens.
 invalid or does not match any expected tokens.
@@ -370,3 +352,5 @@ union UpdateCustomerEmailAddressResult =
     | NativeAuthStrategyError
     | NativeAuthStrategyError
 union RequestPasswordResetResult = Success | NativeAuthStrategyError
 union RequestPasswordResetResult = Success | NativeAuthStrategyError
 union ResetPasswordResult = CurrentUser | PasswordResetTokenInvalidError | PasswordResetTokenExpiredError | NativeAuthStrategyError
 union ResetPasswordResult = CurrentUser | PasswordResetTokenInvalidError | PasswordResetTokenExpiredError | NativeAuthStrategyError
+union NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError
+union AuthenticationResult = CurrentUser | InvalidCredentialsError

+ 0 - 4
packages/core/src/api/schema/type/auth.type.graphql

@@ -1,7 +1,3 @@
-type LoginResult {
-    user: CurrentUser!
-}
-
 type CurrentUser {
 type CurrentUser {
     id: ID!
     id: ID!
     identifier: String!
     identifier: String!

+ 1 - 1
packages/core/src/common/error/error-result.ts

@@ -52,5 +52,5 @@ export function isGraphQlErrorResult<T extends GraphQLErrorResult | U, U = any>(
 export function isGraphQlErrorResult<T, E extends VendureEntity>(
 export function isGraphQlErrorResult<T, E extends VendureEntity>(
     input: ErrorResultUnion<T, E>,
     input: ErrorResultUnion<T, E>,
 ): input is JustErrorResults<ErrorResultUnion<T, E>> {
 ): input is JustErrorResults<ErrorResultUnion<T, E>> {
-    return !!((input as any).code && (input as any).message != null) && (input as any).__typename;
+    return input && !!((input as any).code && (input as any).message != null) && (input as any).__typename;
 }
 }

+ 302 - 6
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -1,8 +1,6 @@
 // tslint:disable
 // tslint:disable
 /** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */
 /** 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';
-
 export type Scalars = {
 export type Scalars = {
   ID: string;
   ID: string;
   String: string;
   String: string;
@@ -16,13 +14,111 @@ export type Scalars = {
 
 
 export class ErrorResult {
 export class ErrorResult {
   readonly __typename: string;
   readonly __typename: string;
-  readonly code: ErrorCode;
+  readonly code: string;
   message: Scalars['String'];
   message: Scalars['String'];
 }
 }
 
 
+export class AlreadyRefundedError extends ErrorResult {
+  readonly __typename = 'AlreadyRefundedError';
+  readonly code = 'ALREADY_REFUNDED_ERROR' as any;
+  readonly message = 'ALREADY_REFUNDED_ERROR';
+  constructor(
+    public   refundId: Scalars['ID'],
+  ) {
+    super();
+  }
+}
+
+export class CancelActiveOrderError extends ErrorResult {
+  readonly __typename = 'CancelActiveOrderError';
+  readonly code = 'CANCEL_ACTIVE_ORDER_ERROR' as any;
+  readonly message = 'CANCEL_ACTIVE_ORDER_ERROR';
+  constructor(
+    public   orderState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class ChannelDefaultLanguageError extends ErrorResult {
+  readonly __typename = 'ChannelDefaultLanguageError';
+  readonly code = 'CHANNEL_DEFAULT_LANGUAGE_ERROR' as any;
+  readonly message = 'CHANNEL_DEFAULT_LANGUAGE_ERROR';
+  constructor(
+    public   language: Scalars['String'],
+    public   channelCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class EmailAddressConflictError extends ErrorResult {
+  readonly __typename = 'EmailAddressConflictError';
+  readonly code = 'EMAIL_ADDRESS_CONFLICT_ERROR' as any;
+  readonly message = 'EMAIL_ADDRESS_CONFLICT_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class EmptyOrderLineSelectionError extends ErrorResult {
+  readonly __typename = 'EmptyOrderLineSelectionError';
+  readonly code = 'EMPTY_ORDER_LINE_SELECTION_ERROR' as any;
+  readonly message = 'EMPTY_ORDER_LINE_SELECTION_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class FulfillmentStateTransitionError extends ErrorResult {
+  readonly __typename = 'FulfillmentStateTransitionError';
+  readonly code = 'FULFILLMENT_STATE_TRANSITION_ERROR' as any;
+  readonly message = 'FULFILLMENT_STATE_TRANSITION_ERROR';
+  constructor(
+    public   transitionError: Scalars['String'],
+    public   fromState: Scalars['String'],
+    public   toState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class InvalidCredentialsError extends ErrorResult {
+  readonly __typename = 'InvalidCredentialsError';
+  readonly code = 'INVALID_CREDENTIALS_ERROR' as any;
+  readonly message = 'INVALID_CREDENTIALS_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class ItemsAlreadyFulfilledError extends ErrorResult {
+  readonly __typename = 'ItemsAlreadyFulfilledError';
+  readonly code = 'ITEMS_ALREADY_FULFILLED_ERROR' as any;
+  readonly message = 'ITEMS_ALREADY_FULFILLED_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class LanguageNotAvailableError extends ErrorResult {
+  readonly __typename = 'LanguageNotAvailableError';
+  readonly code = 'LANGUAGE_NOT_AVAILABLE_ERROR' as any;
+  readonly message = 'LANGUAGE_NOT_AVAILABLE_ERROR';
+  constructor(
+    public   languageCode: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
 export class MimeTypeError extends ErrorResult {
 export class MimeTypeError extends ErrorResult {
   readonly __typename = 'MimeTypeError';
   readonly __typename = 'MimeTypeError';
-  readonly code = ErrorCode.MIME_TYPE_ERROR;
+  readonly code = 'MIME_TYPE_ERROR' as any;
   readonly message = 'MIME_TYPE_ERROR';
   readonly message = 'MIME_TYPE_ERROR';
   constructor(
   constructor(
     public   fileName: Scalars['String'],
     public   fileName: Scalars['String'],
@@ -32,9 +128,49 @@ export class MimeTypeError extends ErrorResult {
   }
   }
 }
 }
 
 
+export class MissingConditionsError extends ErrorResult {
+  readonly __typename = 'MissingConditionsError';
+  readonly code = 'MISSING_CONDITIONS_ERROR' as any;
+  readonly message = 'MISSING_CONDITIONS_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class MultipleOrderError extends ErrorResult {
+  readonly __typename = 'MultipleOrderError';
+  readonly code = 'MULTIPLE_ORDER_ERROR' as any;
+  readonly message = 'MULTIPLE_ORDER_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class NativeAuthStrategyError extends ErrorResult {
+  readonly __typename = 'NativeAuthStrategyError';
+  readonly code = 'NATIVE_AUTH_STRATEGY_ERROR' as any;
+  readonly message = 'NATIVE_AUTH_STRATEGY_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class NothingToRefundError extends ErrorResult {
+  readonly __typename = 'NothingToRefundError';
+  readonly code = 'NOTHING_TO_REFUND_ERROR' as any;
+  readonly message = 'NOTHING_TO_REFUND_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
 export class OrderStateTransitionError extends ErrorResult {
 export class OrderStateTransitionError extends ErrorResult {
   readonly __typename = 'OrderStateTransitionError';
   readonly __typename = 'OrderStateTransitionError';
-  readonly code = ErrorCode.ORDER_STATE_TRANSITION_ERROR;
+  readonly code = 'ORDER_STATE_TRANSITION_ERROR' as any;
   readonly message = 'ORDER_STATE_TRANSITION_ERROR';
   readonly message = 'ORDER_STATE_TRANSITION_ERROR';
   constructor(
   constructor(
     public   transitionError: Scalars['String'],
     public   transitionError: Scalars['String'],
@@ -45,8 +181,88 @@ export class OrderStateTransitionError extends ErrorResult {
   }
   }
 }
 }
 
 
+export class PaymentOrderMismatchError extends ErrorResult {
+  readonly __typename = 'PaymentOrderMismatchError';
+  readonly code = 'PAYMENT_ORDER_MISMATCH_ERROR' as any;
+  readonly message = 'PAYMENT_ORDER_MISMATCH_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
 
 
-const errorTypeNames = new Set(['MimeTypeError', 'OrderStateTransitionError']);
+export class PaymentStateTransitionError extends ErrorResult {
+  readonly __typename = 'PaymentStateTransitionError';
+  readonly code = 'PAYMENT_STATE_TRANSITION_ERROR' as any;
+  readonly message = 'PAYMENT_STATE_TRANSITION_ERROR';
+  constructor(
+    public   transitionError: Scalars['String'],
+    public   fromState: Scalars['String'],
+    public   toState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class ProductOptionInUseError extends ErrorResult {
+  readonly __typename = 'ProductOptionInUseError';
+  readonly code = 'PRODUCT_OPTION_IN_USE_ERROR' as any;
+  readonly message = 'PRODUCT_OPTION_IN_USE_ERROR';
+  constructor(
+    public   optionGroupCode: Scalars['String'],
+    public   productVariantCount: Scalars['Int'],
+  ) {
+    super();
+  }
+}
+
+export class QuantityTooGreatError extends ErrorResult {
+  readonly __typename = 'QuantityTooGreatError';
+  readonly code = 'QUANTITY_TOO_GREAT_ERROR' as any;
+  readonly message = 'QUANTITY_TOO_GREAT_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
+export class RefundOrderStateError extends ErrorResult {
+  readonly __typename = 'RefundOrderStateError';
+  readonly code = 'REFUND_ORDER_STATE_ERROR' as any;
+  readonly message = 'REFUND_ORDER_STATE_ERROR';
+  constructor(
+    public   orderState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class RefundStateTransitionError extends ErrorResult {
+  readonly __typename = 'RefundStateTransitionError';
+  readonly code = 'REFUND_STATE_TRANSITION_ERROR' as any;
+  readonly message = 'REFUND_STATE_TRANSITION_ERROR';
+  constructor(
+    public   transitionError: Scalars['String'],
+    public   fromState: Scalars['String'],
+    public   toState: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+export class SettlePaymentError extends ErrorResult {
+  readonly __typename = 'SettlePaymentError';
+  readonly code = 'SETTLE_PAYMENT_ERROR' as any;
+  readonly message = 'SETTLE_PAYMENT_ERROR';
+  constructor(
+    public   paymentErrorMessage: Scalars['String'],
+  ) {
+    super();
+  }
+}
+
+
+const errorTypeNames = new Set(['MimeTypeError', 'InvalidCredentialsError', 'NativeAuthStrategyError', 'LanguageNotAvailableError', 'EmailAddressConflictError', 'ChannelDefaultLanguageError', 'SettlePaymentError', 'PaymentStateTransitionError', 'OrderStateTransitionError', 'EmptyOrderLineSelectionError', 'ItemsAlreadyFulfilledError', 'QuantityTooGreatError', 'MultipleOrderError', 'CancelActiveOrderError', 'NothingToRefundError', 'PaymentOrderMismatchError', 'RefundOrderStateError', 'AlreadyRefundedError', 'RefundStateTransitionError', 'FulfillmentStateTransitionError', 'ProductOptionInUseError', 'MissingConditionsError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }
 }
@@ -57,9 +273,89 @@ export const adminErrorOperationTypeResolvers = {
       return isGraphQLError(value) ? (value as any).__typename : 'Asset';
       return isGraphQLError(value) ? (value as any).__typename : 'Asset';
     },
     },
   },
   },
+  NativeAuthenticationResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
+  AuthenticationResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
+  CreateChannelResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Channel';
+    },
+  },
+  UpdateChannelResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Channel';
+    },
+  },
+  CreateCustomerResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Customer';
+    },
+  },
+  UpdateCustomerResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Customer';
+    },
+  },
+  UpdateGlobalSettingsResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'GlobalSettings';
+    },
+  },
+  SettlePaymentResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Payment';
+    },
+  },
+  AddFulfillmentToOrderResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Fulfillment';
+    },
+  },
+  CancelOrderResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Order';
+    },
+  },
+  RefundOrderResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Refund';
+    },
+  },
+  SettleRefundResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Refund';
+    },
+  },
   TransitionOrderToStateResult: {
   TransitionOrderToStateResult: {
     __resolveType(value: any) {
     __resolveType(value: any) {
       return isGraphQLError(value) ? (value as any).__typename : 'Order';
       return isGraphQLError(value) ? (value as any).__typename : 'Order';
     },
     },
   },
   },
+  TransitionFulfillmentToStateResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Fulfillment';
+    },
+  },
+  RemoveOptionGroupFromProductResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Product';
+    },
+  },
+  CreatePromotionResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Promotion';
+    },
+  },
+  UpdatePromotionResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'Promotion';
+    },
+  },
 };
 };

+ 34 - 26
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -1,8 +1,6 @@
 // tslint:disable
 // tslint:disable
 /** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */
 /** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */
 
 
-import { ErrorCode } from '@vendure/common/lib/generated-shop-types';
-
 export type Scalars = {
 export type Scalars = {
   ID: string;
   ID: string;
   String: string;
   String: string;
@@ -16,13 +14,13 @@ export type Scalars = {
 
 
 export class ErrorResult {
 export class ErrorResult {
   readonly __typename: string;
   readonly __typename: string;
-  readonly code: ErrorCode;
+  readonly code: string;
   message: Scalars['String'];
   message: Scalars['String'];
 }
 }
 
 
 export class AlreadyLoggedInError extends ErrorResult {
 export class AlreadyLoggedInError extends ErrorResult {
   readonly __typename = 'AlreadyLoggedInError';
   readonly __typename = 'AlreadyLoggedInError';
-  readonly code = ErrorCode.ALREADY_LOGGED_IN_ERROR;
+  readonly code = 'ALREADY_LOGGED_IN_ERROR' as any;
   readonly message = 'ALREADY_LOGGED_IN_ERROR';
   readonly message = 'ALREADY_LOGGED_IN_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -32,7 +30,7 @@ export class AlreadyLoggedInError extends ErrorResult {
 
 
 export class CouponCodeExpiredError extends ErrorResult {
 export class CouponCodeExpiredError extends ErrorResult {
   readonly __typename = 'CouponCodeExpiredError';
   readonly __typename = 'CouponCodeExpiredError';
-  readonly code = ErrorCode.COUPON_CODE_EXPIRED_ERROR;
+  readonly code = 'COUPON_CODE_EXPIRED_ERROR' as any;
   readonly message = 'COUPON_CODE_EXPIRED_ERROR';
   readonly message = 'COUPON_CODE_EXPIRED_ERROR';
   constructor(
   constructor(
     public   couponCode: Scalars['String'],
     public   couponCode: Scalars['String'],
@@ -43,7 +41,7 @@ export class CouponCodeExpiredError extends ErrorResult {
 
 
 export class CouponCodeInvalidError extends ErrorResult {
 export class CouponCodeInvalidError extends ErrorResult {
   readonly __typename = 'CouponCodeInvalidError';
   readonly __typename = 'CouponCodeInvalidError';
-  readonly code = ErrorCode.COUPON_CODE_INVALID_ERROR;
+  readonly code = 'COUPON_CODE_INVALID_ERROR' as any;
   readonly message = 'COUPON_CODE_INVALID_ERROR';
   readonly message = 'COUPON_CODE_INVALID_ERROR';
   constructor(
   constructor(
     public   couponCode: Scalars['String'],
     public   couponCode: Scalars['String'],
@@ -54,7 +52,7 @@ export class CouponCodeInvalidError extends ErrorResult {
 
 
 export class CouponCodeLimitError extends ErrorResult {
 export class CouponCodeLimitError extends ErrorResult {
   readonly __typename = 'CouponCodeLimitError';
   readonly __typename = 'CouponCodeLimitError';
-  readonly code = ErrorCode.COUPON_CODE_LIMIT_ERROR;
+  readonly code = 'COUPON_CODE_LIMIT_ERROR' as any;
   readonly message = 'COUPON_CODE_LIMIT_ERROR';
   readonly message = 'COUPON_CODE_LIMIT_ERROR';
   constructor(
   constructor(
     public   couponCode: Scalars['String'],
     public   couponCode: Scalars['String'],
@@ -66,7 +64,7 @@ export class CouponCodeLimitError extends ErrorResult {
 
 
 export class EmailAddressConflictError extends ErrorResult {
 export class EmailAddressConflictError extends ErrorResult {
   readonly __typename = 'EmailAddressConflictError';
   readonly __typename = 'EmailAddressConflictError';
-  readonly code = ErrorCode.EMAIL_ADDRESS_CONFLICT_ERROR;
+  readonly code = 'EMAIL_ADDRESS_CONFLICT_ERROR' as any;
   readonly message = 'EMAIL_ADDRESS_CONFLICT_ERROR';
   readonly message = 'EMAIL_ADDRESS_CONFLICT_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -76,7 +74,7 @@ export class EmailAddressConflictError extends ErrorResult {
 
 
 export class IdentifierChangeTokenExpiredError extends ErrorResult {
 export class IdentifierChangeTokenExpiredError extends ErrorResult {
   readonly __typename = 'IdentifierChangeTokenExpiredError';
   readonly __typename = 'IdentifierChangeTokenExpiredError';
-  readonly code = ErrorCode.IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR;
+  readonly code = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR' as any;
   readonly message = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR';
   readonly message = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -86,7 +84,7 @@ export class IdentifierChangeTokenExpiredError extends ErrorResult {
 
 
 export class IdentifierChangeTokenInvalidError extends ErrorResult {
 export class IdentifierChangeTokenInvalidError extends ErrorResult {
   readonly __typename = 'IdentifierChangeTokenInvalidError';
   readonly __typename = 'IdentifierChangeTokenInvalidError';
-  readonly code = ErrorCode.IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR;
+  readonly code = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR' as any;
   readonly message = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR';
   readonly message = 'IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -96,7 +94,7 @@ export class IdentifierChangeTokenInvalidError extends ErrorResult {
 
 
 export class InvalidCredentialsError extends ErrorResult {
 export class InvalidCredentialsError extends ErrorResult {
   readonly __typename = 'InvalidCredentialsError';
   readonly __typename = 'InvalidCredentialsError';
-  readonly code = ErrorCode.INVALID_CREDENTIALS_ERROR;
+  readonly code = 'INVALID_CREDENTIALS_ERROR' as any;
   readonly message = 'INVALID_CREDENTIALS_ERROR';
   readonly message = 'INVALID_CREDENTIALS_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -106,7 +104,7 @@ export class InvalidCredentialsError extends ErrorResult {
 
 
 export class MissingPasswordError extends ErrorResult {
 export class MissingPasswordError extends ErrorResult {
   readonly __typename = 'MissingPasswordError';
   readonly __typename = 'MissingPasswordError';
-  readonly code = ErrorCode.MISSING_PASSWORD_ERROR;
+  readonly code = 'MISSING_PASSWORD_ERROR' as any;
   readonly message = 'MISSING_PASSWORD_ERROR';
   readonly message = 'MISSING_PASSWORD_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -116,7 +114,7 @@ export class MissingPasswordError extends ErrorResult {
 
 
 export class NativeAuthStrategyError extends ErrorResult {
 export class NativeAuthStrategyError extends ErrorResult {
   readonly __typename = 'NativeAuthStrategyError';
   readonly __typename = 'NativeAuthStrategyError';
-  readonly code = ErrorCode.NATIVE_AUTH_STRATEGY_ERROR;
+  readonly code = 'NATIVE_AUTH_STRATEGY_ERROR' as any;
   readonly message = 'NATIVE_AUTH_STRATEGY_ERROR';
   readonly message = 'NATIVE_AUTH_STRATEGY_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -126,7 +124,7 @@ export class NativeAuthStrategyError extends ErrorResult {
 
 
 export class NegativeQuantityError extends ErrorResult {
 export class NegativeQuantityError extends ErrorResult {
   readonly __typename = 'NegativeQuantityError';
   readonly __typename = 'NegativeQuantityError';
-  readonly code = ErrorCode.NEGATIVE_QUANTITY_ERROR;
+  readonly code = 'NEGATIVE_QUANTITY_ERROR' as any;
   readonly message = 'NEGATIVE_QUANTITY_ERROR';
   readonly message = 'NEGATIVE_QUANTITY_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -136,7 +134,7 @@ export class NegativeQuantityError extends ErrorResult {
 
 
 export class OrderLimitError extends ErrorResult {
 export class OrderLimitError extends ErrorResult {
   readonly __typename = 'OrderLimitError';
   readonly __typename = 'OrderLimitError';
-  readonly code = ErrorCode.ORDER_LIMIT_ERROR;
+  readonly code = 'ORDER_LIMIT_ERROR' as any;
   readonly message = 'ORDER_LIMIT_ERROR';
   readonly message = 'ORDER_LIMIT_ERROR';
   constructor(
   constructor(
     public   maxItems: Scalars['Int'],
     public   maxItems: Scalars['Int'],
@@ -147,7 +145,7 @@ export class OrderLimitError extends ErrorResult {
 
 
 export class OrderModificationError extends ErrorResult {
 export class OrderModificationError extends ErrorResult {
   readonly __typename = 'OrderModificationError';
   readonly __typename = 'OrderModificationError';
-  readonly code = ErrorCode.ORDER_MODIFICATION_ERROR;
+  readonly code = 'ORDER_MODIFICATION_ERROR' as any;
   readonly message = 'ORDER_MODIFICATION_ERROR';
   readonly message = 'ORDER_MODIFICATION_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -157,7 +155,7 @@ export class OrderModificationError extends ErrorResult {
 
 
 export class OrderPaymentStateError extends ErrorResult {
 export class OrderPaymentStateError extends ErrorResult {
   readonly __typename = 'OrderPaymentStateError';
   readonly __typename = 'OrderPaymentStateError';
-  readonly code = ErrorCode.ORDER_PAYMENT_STATE_ERROR;
+  readonly code = 'ORDER_PAYMENT_STATE_ERROR' as any;
   readonly message = 'ORDER_PAYMENT_STATE_ERROR';
   readonly message = 'ORDER_PAYMENT_STATE_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -167,7 +165,7 @@ export class OrderPaymentStateError extends ErrorResult {
 
 
 export class OrderStateTransitionError extends ErrorResult {
 export class OrderStateTransitionError extends ErrorResult {
   readonly __typename = 'OrderStateTransitionError';
   readonly __typename = 'OrderStateTransitionError';
-  readonly code = ErrorCode.ORDER_STATE_TRANSITION_ERROR;
+  readonly code = 'ORDER_STATE_TRANSITION_ERROR' as any;
   readonly message = 'ORDER_STATE_TRANSITION_ERROR';
   readonly message = 'ORDER_STATE_TRANSITION_ERROR';
   constructor(
   constructor(
     public   transitionError: Scalars['String'],
     public   transitionError: Scalars['String'],
@@ -180,7 +178,7 @@ export class OrderStateTransitionError extends ErrorResult {
 
 
 export class PasswordAlreadySetError extends ErrorResult {
 export class PasswordAlreadySetError extends ErrorResult {
   readonly __typename = 'PasswordAlreadySetError';
   readonly __typename = 'PasswordAlreadySetError';
-  readonly code = ErrorCode.PASSWORD_ALREADY_SET_ERROR;
+  readonly code = 'PASSWORD_ALREADY_SET_ERROR' as any;
   readonly message = 'PASSWORD_ALREADY_SET_ERROR';
   readonly message = 'PASSWORD_ALREADY_SET_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -190,7 +188,7 @@ export class PasswordAlreadySetError extends ErrorResult {
 
 
 export class PasswordResetTokenExpiredError extends ErrorResult {
 export class PasswordResetTokenExpiredError extends ErrorResult {
   readonly __typename = 'PasswordResetTokenExpiredError';
   readonly __typename = 'PasswordResetTokenExpiredError';
-  readonly code = ErrorCode.PASSWORD_RESET_TOKEN_EXPIRED_ERROR;
+  readonly code = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR' as any;
   readonly message = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR';
   readonly message = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -200,7 +198,7 @@ export class PasswordResetTokenExpiredError extends ErrorResult {
 
 
 export class PasswordResetTokenInvalidError extends ErrorResult {
 export class PasswordResetTokenInvalidError extends ErrorResult {
   readonly __typename = 'PasswordResetTokenInvalidError';
   readonly __typename = 'PasswordResetTokenInvalidError';
-  readonly code = ErrorCode.PASSWORD_RESET_TOKEN_INVALID_ERROR;
+  readonly code = 'PASSWORD_RESET_TOKEN_INVALID_ERROR' as any;
   readonly message = 'PASSWORD_RESET_TOKEN_INVALID_ERROR';
   readonly message = 'PASSWORD_RESET_TOKEN_INVALID_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -210,7 +208,7 @@ export class PasswordResetTokenInvalidError extends ErrorResult {
 
 
 export class PaymentDeclinedError extends ErrorResult {
 export class PaymentDeclinedError extends ErrorResult {
   readonly __typename = 'PaymentDeclinedError';
   readonly __typename = 'PaymentDeclinedError';
-  readonly code = ErrorCode.PAYMENT_DECLINED_ERROR;
+  readonly code = 'PAYMENT_DECLINED_ERROR' as any;
   readonly message = 'PAYMENT_DECLINED_ERROR';
   readonly message = 'PAYMENT_DECLINED_ERROR';
   constructor(
   constructor(
     public   paymentErrorMessage: Scalars['String'],
     public   paymentErrorMessage: Scalars['String'],
@@ -221,7 +219,7 @@ export class PaymentDeclinedError extends ErrorResult {
 
 
 export class PaymentFailedError extends ErrorResult {
 export class PaymentFailedError extends ErrorResult {
   readonly __typename = 'PaymentFailedError';
   readonly __typename = 'PaymentFailedError';
-  readonly code = ErrorCode.PAYMENT_FAILED_ERROR;
+  readonly code = 'PAYMENT_FAILED_ERROR' as any;
   readonly message = 'PAYMENT_FAILED_ERROR';
   readonly message = 'PAYMENT_FAILED_ERROR';
   constructor(
   constructor(
     public   paymentErrorMessage: Scalars['String'],
     public   paymentErrorMessage: Scalars['String'],
@@ -232,7 +230,7 @@ export class PaymentFailedError extends ErrorResult {
 
 
 export class VerificationTokenExpiredError extends ErrorResult {
 export class VerificationTokenExpiredError extends ErrorResult {
   readonly __typename = 'VerificationTokenExpiredError';
   readonly __typename = 'VerificationTokenExpiredError';
-  readonly code = ErrorCode.VERIFICATION_TOKEN_EXPIRED_ERROR;
+  readonly code = 'VERIFICATION_TOKEN_EXPIRED_ERROR' as any;
   readonly message = 'VERIFICATION_TOKEN_EXPIRED_ERROR';
   readonly message = 'VERIFICATION_TOKEN_EXPIRED_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -242,7 +240,7 @@ export class VerificationTokenExpiredError extends ErrorResult {
 
 
 export class VerificationTokenInvalidError extends ErrorResult {
 export class VerificationTokenInvalidError extends ErrorResult {
   readonly __typename = 'VerificationTokenInvalidError';
   readonly __typename = 'VerificationTokenInvalidError';
-  readonly code = ErrorCode.VERIFICATION_TOKEN_INVALID_ERROR;
+  readonly code = 'VERIFICATION_TOKEN_INVALID_ERROR' as any;
   readonly message = 'VERIFICATION_TOKEN_INVALID_ERROR';
   readonly message = 'VERIFICATION_TOKEN_INVALID_ERROR';
   constructor(
   constructor(
   ) {
   ) {
@@ -251,7 +249,7 @@ export class VerificationTokenInvalidError extends ErrorResult {
 }
 }
 
 
 
 
-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']);
+const errorTypeNames = new Set(['OrderModificationError', 'OrderLimitError', 'NegativeQuantityError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'OrderStateTransitionError', 'OrderPaymentStateError', 'PaymentFailedError', 'PaymentDeclinedError', 'AlreadyLoggedInError', 'EmailAddressConflictError', 'InvalidCredentialsError', 'NativeAuthStrategyError', 'MissingPasswordError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'PasswordAlreadySetError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }
 }
@@ -292,6 +290,16 @@ export const shopErrorOperationTypeResolvers = {
       return isGraphQLError(value) ? (value as any).__typename : 'Order';
       return isGraphQLError(value) ? (value as any).__typename : 'Order';
     },
     },
   },
   },
+  NativeAuthenticationResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
+  AuthenticationResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';
+    },
+  },
   RegisterCustomerAccountResult: {
   RegisterCustomerAccountResult: {
     __resolveType(value: any) {
     __resolveType(value: any) {
       return isGraphQLError(value) ? (value as any).__typename : 'Success';
       return isGraphQLError(value) ? (value as any).__typename : 'Success';

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

@@ -2,6 +2,7 @@ export * from './finite-state-machine/finite-state-machine';
 export * from './finite-state-machine/types';
 export * from './finite-state-machine/types';
 export * from './async-queue';
 export * from './async-queue';
 export * from './error/errors';
 export * from './error/errors';
+export * from './error/error-result';
 export * from './error/generated-graphql-admin-errors';
 export * from './error/generated-graphql-admin-errors';
 export * from './injector';
 export * from './injector';
 export * from './ttl-cache';
 export * from './ttl-cache';

+ 5 - 6
packages/core/src/config/auth/native-authentication-strategy.ts

@@ -49,6 +49,9 @@ export class NativeAuthenticationStrategy implements AuthenticationStrategy<Nati
 
 
     async authenticate(ctx: RequestContext, data: NativeAuthenticationData): Promise<User | false> {
     async authenticate(ctx: RequestContext, data: NativeAuthenticationData): Promise<User | false> {
         const user = await this.getUserFromIdentifier(ctx, data.username);
         const user = await this.getUserFromIdentifier(ctx, data.username);
+        if (!user) {
+            return false;
+        }
         const passwordMatch = await this.verifyUserPassword(ctx, user.id, data.password);
         const passwordMatch = await this.verifyUserPassword(ctx, user.id, data.password);
         if (!passwordMatch) {
         if (!passwordMatch) {
             return false;
             return false;
@@ -56,15 +59,11 @@ export class NativeAuthenticationStrategy implements AuthenticationStrategy<Nati
         return user;
         return user;
     }
     }
 
 
-    private async getUserFromIdentifier(ctx: RequestContext, identifier: string): Promise<User> {
-        const user = await this.connection.getRepository(ctx, User).findOne({
+    private getUserFromIdentifier(ctx: RequestContext, identifier: string): Promise<User | undefined> {
+        return this.connection.getRepository(ctx, User).findOne({
             where: { identifier },
             where: { identifier },
             relations: ['roles', 'roles.channels'],
             relations: ['roles', 'roles.channels'],
         });
         });
-        if (!user) {
-            throw new UnauthorizedError();
-        }
-        return user;
     }
     }
 
 
     /**
     /**

+ 15 - 28
packages/core/src/i18n/messages/en.json

@@ -1,30 +1,18 @@
 {
 {
   "error": {
   "error": {
-    "cancel-order-lines-invalid-order-state": "Cannot cancel OrderLines from an Order in the \"{ state }\" state",
-    "cancel-order-lines-nothing-to-cancel": "Nothing to cancel",
-    "cancel-order-lines-quantity-too-high": "Quantity to cancel is greater than existing OrderLine quantity",
     "cannot-delete-role": "The role '{ roleCode }' cannot be deleted",
     "cannot-delete-role": "The role '{ roleCode }' cannot be deleted",
     "cannot-locate-customer-for-user": "Cannot locate a Customer for the user",
     "cannot-locate-customer-for-user": "Cannot locate a Customer for the user",
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
     "cannot-modify-role": "The role '{ roleCode }' cannot be modified",
     "cannot-create-sales-for-active-order": "Cannot create a Sale for an Order which is still active",
     "cannot-create-sales-for-active-order": "Cannot create a Sale for an Order which is still active",
     "cannot-move-collection-into-self": "Cannot move a Collection into itself",
     "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-default-language-as-unavailable": "Cannot remove make language \"{ language }\" unavailable as it is used as the defaultLanguage by the channel \"{channelCode}\"",
     "cannot-transition-payment-from-to": "Cannot transition Payment 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-refund-from-to": "Cannot transition Refund from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-fulfillment-from-to": "Cannot transition Fulfillment from \"{ fromState }\" to \"{ toState }\"",
     "cannot-transition-fulfillment-from-to": "Cannot transition Fulfillment from \"{ fromState }\" to \"{ toState }\"",
-    "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-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",
     "collection-id-slug-mismatch": "The provided id and slug refer to different Collections",
     "country-code-not-valid": "The countryCode \"{ countryCode }\" was not recognized",
     "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",
     "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",
     "email-address-not-verified": "Please verify this email address before logging in",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
@@ -37,18 +25,10 @@
     "field-invalid-string-option": "The custom field '{ name }' value ['{ value }'] is invalid. Valid options are [{ validOptions }]",
     "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 }]",
     "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",
     "forbidden": "You are not currently authorized to perform this action",
-    "identifier-change-token-has-expired": "Identifier change token has expired",
     "invalid-sort-field": "The sort field '{ fieldName }' is invalid. Valid fields are: { validFields }",
     "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.",
     "no-active-tax-zone": "The active tax zone could not be determined. Ensure a default tax zone is set for the current channel.",
     "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-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-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
-    "order-lines-must-belong-to-same-order": "OrderLines must all belong to a single Order",
-    "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",
     "pending-identifier-missing": "Could not find the pending email address to update",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
     "permission-invalid": "The permission \"{ permission }\" is not valid",
     "products-cannot-be-removed-from-default-channel": "Products cannot be removed from the default Channel",
     "products-cannot-be-removed-from-default-channel": "Products cannot be removed from the default Channel",
@@ -56,28 +36,29 @@
     "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-id-slug-mismatch": "The provided id and slug refer to different Products",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",
     "product-variant-option-ids-not-compatible": "ProductVariant optionIds must include one optionId from each of the groups: {groupNames}",
     "product-variant-options-combination-already-exists": "A ProductVariant already exists with the options: {optionNames}",
     "product-variant-options-combination-already-exists": "A ProductVariant already exists with the options: {optionNames}",
-    "promotion-must-have-conditions-or-coupon-code": "A Promotion must have either at least one condition or a coupon code set",
-    "refund-order-item-already-refunded": "Cannot refund an OrderItem which has already been refunded",
-    "refund-order-lines-invalid-order-state": "Cannot refund an Order in the \"{ state }\" state",
-    "refund-order-lines-nothing-to-refund": "Nothing to refund",
-    "refund-order-lines-quantity-too-high": "Quantity to refund is greater than existing OrderLine quantity",
-    "refund-order-payment-lines-mismatch": "The Payment and OrderLines do not belong to the same Order",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
     "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
-    "verification-token-has-expired": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
-    "verification-token-not-recognized": "Verification token not recognized",
     "unauthorized": "The credentials did not match. Please check and try again"
     "unauthorized": "The credentials did not match. Please check and try again"
   },
   },
   "errorResult": {
   "errorResult": {
+    "ALREADY_CANCELLED_ERROR": "Quantity to cancel is greater than existing OrderLine quantity",
     "ALREADY_LOGGED_IN_ERROR": "Cannot set a Customer for the Order when already logged in",
     "ALREADY_LOGGED_IN_ERROR": "Cannot set a Customer for the Order when already logged in",
+    "ALREADY_REFUNDED_ERROR": "Cannot refund an OrderItem which has already been refunded",
+    "CANCEL_ACTIVE_ORDER_ERROR": "Cannot cancel OrderLines from an Order in the \"{ orderState }\" state",
+    "CHANNEL_DEFAULT_LANGUAGE_ERROR": "Cannot make language \"{ language }\" unavailable as it is used as the defaultLanguage by the channel \"{ channelCode }\"",
     "COUPON_CODE_EXPIRED_ERROR": "Coupon code \"{ couponCode }\" has expired",
     "COUPON_CODE_EXPIRED_ERROR": "Coupon code \"{ couponCode }\" has expired",
     "COUPON_CODE_INVALID_ERROR": "Coupon code \"{ couponCode }\" is not valid",
     "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",
     "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.",
     "EMAIL_ADDRESS_CONFLICT_ERROR": "The email address is not available.",
+    "EMPTY_ORDER_LINE_SELECTION_ERROR": "At least one OrderLine must be specified",
     "IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR": "Identifier change token not recognized",
     "IDENTIFIER_CHANGE_TOKEN_INVALID_ERROR": "Identifier change token not recognized",
     "INVALID_CREDENTIALS_ERROR": "The provided credentials are invalid",
     "INVALID_CREDENTIALS_ERROR": "The provided credentials are invalid",
+    "ITEMS_ALREADY_FULFILLED_ERROR": "One or more OrderItems are already part of a Fulfillment",
+    "LANGUAGE_NOT_AVAILABLE_ERROR": "Language \"{languageCode}\" is not available. First enable it via GlobalSettings and try again",
     "MIME_TYPE_ERROR": "The MIME type '{ mimeType }' is not permitted.",
     "MIME_TYPE_ERROR": "The MIME type '{ mimeType }' is not permitted.",
+    "MISSING_CONDITIONS_ERROR": "A Promotion must have either at least one condition or a coupon code set",
     "MISSING_PASSWORD_ERROR": "A password must be provided.",
     "MISSING_PASSWORD_ERROR": "A password must be provided.",
     "NEGATIVE_QUANTITY_ERROR": "The quantity for an OrderItem cannot be negative",
     "NEGATIVE_QUANTITY_ERROR": "The quantity for an OrderItem cannot be negative",
+    "NOTHING_TO_REFUND_ERROR": "Nothing to refund",
     "ORDER_LIMIT_ERROR": "Cannot add items. An order may consist of a maximum of { maxItems } items",
     "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_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_PAYMENT_STATE_ERROR": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
@@ -87,11 +68,17 @@
     "PASSWORD_RESET_TOKEN_INVALID_ERROR": "Password reset token not recognized",
     "PASSWORD_RESET_TOKEN_INVALID_ERROR": "Password reset token not recognized",
     "PAYMENT_DECLINED_ERROR": "The payment was declined",
     "PAYMENT_DECLINED_ERROR": "The payment was declined",
     "PAYMENT_FAILED_ERROR": "The payment failed",
     "PAYMENT_FAILED_ERROR": "The payment failed",
+    "PAYMENT_ORDER_MISMATCH_ERROR": "The Payment and OrderLines do not belong to the same Order",
+    "PRODUCT_OPTION_IN_USE_ERROR": "Cannot remove ProductOptionGroup \"{ optionGroupCode }\" as it is used by {productVariantCount, plural, one {1 ProductVariant} other {# ProductVariants}}",
+    "QUANTITY_TOO_GREAT_ERROR": "The specified quantity is greater than the available OrderItems",
+    "REFUND_ORDER_STATE_ERROR": "Cannot refund an Order in the \"{ orderState }\" state",
+    "SETTLE_PAYMENT_ERROR": "Settling the payment failed",
     "VERIFICATION_TOKEN_EXPIRED_ERROR": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "VERIFICATION_TOKEN_EXPIRED_ERROR": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "VERIFICATION_TOKEN_INVALID_ERROR": "Verification token not recognized"
     "VERIFICATION_TOKEN_INVALID_ERROR": "Verification token not recognized"
   },
   },
   "message": {
   "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}}",
     "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-remove-tax-category-due-to-tax-rates": "Cannot remove TaxCategory \"{ name }\" as it is referenced by {count, plural, one {1 TaxRate} other {# TaxRates}}",
     "cannot-transition-order-from-to": "Cannot transition Order from \"{ fromState }\" to \"{ toState }\"",
     "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-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-to-payment-without-customer": "Cannot transition Order to the \"ArrangingPayment\" state without Customer details",

+ 6 - 5
packages/core/src/service/services/auth.service.ts

@@ -3,8 +3,9 @@ import { ID } from '@vendure/common/lib/shared-types';
 
 
 import { ApiType } from '../../api/common/get-api-type';
 import { ApiType } from '../../api/common/get-api-type';
 import { RequestContext } from '../../api/common/request-context';
 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 { InternalServerError, NotVerifiedError } from '../../common/error/errors';
+import { InvalidCredentialsError } from '../../common/error/generated-graphql-admin-errors';
+import { InvalidCredentialsError as ShopInvalidCredentialsError } from '../../common/error/generated-graphql-shop-errors';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import {
 import {
     NATIVE_AUTH_STRATEGY_NAME,
     NATIVE_AUTH_STRATEGY_NAME,
@@ -42,7 +43,7 @@ export class AuthService {
         apiType: ApiType,
         apiType: ApiType,
         authenticationMethod: string,
         authenticationMethod: string,
         authenticationData: any,
         authenticationData: any,
-    ): Promise<AuthenticatedSession> {
+    ): Promise<AuthenticatedSession | InvalidCredentialsError> {
         this.eventBus.publish(
         this.eventBus.publish(
             new AttemptedLoginEvent(
             new AttemptedLoginEvent(
                 ctx,
                 ctx,
@@ -55,7 +56,7 @@ export class AuthService {
         const authenticationStrategy = this.getAuthenticationStrategy(apiType, authenticationMethod);
         const authenticationStrategy = this.getAuthenticationStrategy(apiType, authenticationMethod);
         const user = await authenticationStrategy.authenticate(ctx, authenticationData);
         const user = await authenticationStrategy.authenticate(ctx, authenticationData);
         if (!user) {
         if (!user) {
-            throw new UnauthorizedError();
+            return new InvalidCredentialsError();
         }
         }
         return this.createAuthenticatedSessionForUser(ctx, user, authenticationStrategy.name);
         return this.createAuthenticatedSessionForUser(ctx, user, authenticationStrategy.name);
     }
     }
@@ -100,7 +101,7 @@ export class AuthService {
         ctx: RequestContext,
         ctx: RequestContext,
         userId: ID,
         userId: ID,
         password: string,
         password: string,
-    ): Promise<boolean | InvalidCredentialsError> {
+    ): Promise<boolean | InvalidCredentialsError | ShopInvalidCredentialsError> {
         const nativeAuthenticationStrategy = this.getAuthenticationStrategy(
         const nativeAuthenticationStrategy = this.getAuthenticationStrategy(
             'shop',
             'shop',
             NATIVE_AUTH_STRATEGY_NAME,
             NATIVE_AUTH_STRATEGY_NAME,

+ 34 - 17
packages/core/src/service/services/channel.service.ts

@@ -1,22 +1,21 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
 import {
 import {
     CreateChannelInput,
     CreateChannelInput,
+    CreateChannelResult,
     CurrencyCode,
     CurrencyCode,
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
     UpdateChannelInput,
     UpdateChannelInput,
+    UpdateChannelResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { ID, Type } from '@vendure/common/lib/shared-types';
 import { ID, Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
-import {
-    ChannelNotFoundError,
-    EntityNotFoundError,
-    InternalServerError,
-    UserInputError,
-} from '../../common/error/errors';
+import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
+import { ChannelNotFoundError, EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import { LanguageNotAvailableError } from '../../common/error/generated-graphql-admin-errors';
 import { ChannelAware } from '../../common/types/common-types';
 import { ChannelAware } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
@@ -136,8 +135,15 @@ export class ChannelService {
             .findOne(id, { relations: ['defaultShippingZone', 'defaultTaxZone'] });
             .findOne(id, { relations: ['defaultShippingZone', 'defaultTaxZone'] });
     }
     }
 
 
-    async create(ctx: RequestContext, input: CreateChannelInput): Promise<Channel> {
+    async create(
+        ctx: RequestContext,
+        input: CreateChannelInput,
+    ): Promise<ErrorResultUnion<CreateChannelResult, Channel>> {
         const channel = new Channel(input);
         const channel = new Channel(input);
+        const defaultLanguageValidationResult = await this.validateDefaultLanguageCode(ctx, input);
+        if (isGraphQlErrorResult(defaultLanguageValidationResult)) {
+            return defaultLanguageValidationResult;
+        }
         if (input.defaultTaxZoneId) {
         if (input.defaultTaxZoneId) {
             channel.defaultTaxZone = await this.connection.getEntityOrThrow(
             channel.defaultTaxZone = await this.connection.getEntityOrThrow(
                 ctx,
                 ctx,
@@ -157,20 +163,17 @@ export class ChannelService {
         return channel;
         return channel;
     }
     }
 
 
-    async update(ctx: RequestContext, input: UpdateChannelInput): Promise<Channel> {
+    async update(
+        ctx: RequestContext,
+        input: UpdateChannelInput,
+    ): Promise<ErrorResultUnion<UpdateChannelResult, Channel>> {
         const channel = await this.findOne(ctx, input.id);
         const channel = await this.findOne(ctx, input.id);
         if (!channel) {
         if (!channel) {
             throw new EntityNotFoundError('Channel', input.id);
             throw new EntityNotFoundError('Channel', input.id);
         }
         }
-        if (input.defaultLanguageCode) {
-            const availableLanguageCodes = await this.globalSettingsService
-                .getSettings(ctx)
-                .then(s => s.availableLanguages);
-            if (!availableLanguageCodes.includes(input.defaultLanguageCode)) {
-                throw new UserInputError('error.language-not-available-in-global-settings', {
-                    code: input.defaultLanguageCode,
-                });
-            }
+        const defaultLanguageValidationResult = await this.validateDefaultLanguageCode(ctx, input);
+        if (isGraphQlErrorResult(defaultLanguageValidationResult)) {
+            return defaultLanguageValidationResult;
         }
         }
         const updatedChannel = patchEntity(channel, input);
         const updatedChannel = patchEntity(channel, input);
         if (input.defaultTaxZoneId) {
         if (input.defaultTaxZoneId) {
@@ -230,6 +233,20 @@ export class ChannelService {
         }
         }
     }
     }
 
 
+    private async validateDefaultLanguageCode(
+        ctx: RequestContext,
+        input: CreateChannelInput | UpdateChannelInput,
+    ): Promise<LanguageNotAvailableError | undefined> {
+        if (input.defaultLanguageCode) {
+            const availableLanguageCodes = await this.globalSettingsService
+                .getSettings(ctx)
+                .then(s => s.availableLanguages);
+            if (!availableLanguageCodes.includes(input.defaultLanguageCode)) {
+                return new LanguageNotAvailableError(input.defaultLanguageCode);
+            }
+        }
+    }
+
     private async updateAllChannels(ctx?: RequestContext) {
     private async updateAllChannels(ctx?: RequestContext) {
         this.allChannels = await this.findAll(ctx || RequestContext.empty());
         this.allChannels = await this.findAll(ctx || RequestContext.empty());
     }
     }

+ 59 - 28
packages/core/src/service/services/customer.service.ts

@@ -2,29 +2,28 @@ import { Injectable } from '@nestjs/common';
 import {
 import {
     RegisterCustomerAccountResult,
     RegisterCustomerAccountResult,
     RegisterCustomerInput,
     RegisterCustomerInput,
+    UpdateCustomerInput as UpdateCustomerShopInput,
     VerifyCustomerAccountResult,
     VerifyCustomerAccountResult,
 } from '@vendure/common/lib/generated-shop-types';
 } from '@vendure/common/lib/generated-shop-types';
 import {
 import {
     AddNoteToCustomerInput,
     AddNoteToCustomerInput,
     CreateAddressInput,
     CreateAddressInput,
     CreateCustomerInput,
     CreateCustomerInput,
+    CreateCustomerResult,
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
     HistoryEntryType,
     HistoryEntryType,
     UpdateAddressInput,
     UpdateAddressInput,
     UpdateCustomerInput,
     UpdateCustomerInput,
     UpdateCustomerNoteInput,
     UpdateCustomerNoteInput,
+    UpdateCustomerResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
 import { ErrorResultUnion, isGraphQlErrorResult } from '../../common/error/error-result';
-import {
-    EntityNotFoundError,
-    IllegalOperationError,
-    InternalServerError,
-    UserInputError,
-} from '../../common/error/errors';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import { EmailAddressConflictError as EmailAddressConflictAdminError } from '../../common/error/generated-graphql-admin-errors';
 import {
 import {
     EmailAddressConflictError,
     EmailAddressConflictError,
     IdentifierChangeTokenExpiredError,
     IdentifierChangeTokenExpiredError,
@@ -32,7 +31,6 @@ import {
     MissingPasswordError,
     MissingPasswordError,
     PasswordResetTokenExpiredError,
     PasswordResetTokenExpiredError,
     PasswordResetTokenInvalidError,
     PasswordResetTokenInvalidError,
-    VerificationTokenInvalidError,
 } from '../../common/error/generated-graphql-shop-errors';
 } from '../../common/error/generated-graphql-shop-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
 import { assertFound, idsAreEqual, normalizeEmailAddress } from '../../common/utils';
@@ -145,7 +143,11 @@ export class CustomerService {
         }
         }
     }
     }
 
 
-    async create(ctx: RequestContext, input: CreateCustomerInput, password?: string): Promise<Customer> {
+    async create(
+        ctx: RequestContext,
+        input: CreateCustomerInput,
+        password?: string,
+    ): Promise<ErrorResultUnion<CreateCustomerResult, Customer>> {
         input.emailAddress = normalizeEmailAddress(input.emailAddress);
         input.emailAddress = normalizeEmailAddress(input.emailAddress);
         const customer = new Customer(input);
         const customer = new Customer(input);
 
 
@@ -159,7 +161,7 @@ export class CustomerService {
             .getOne();
             .getOne();
 
 
         if (existingCustomerInChannel) {
         if (existingCustomerInChannel) {
-            throw new UserInputError(`error.email-address-must-be-unique`);
+            return new EmailAddressConflictAdminError();
         }
         }
 
 
         const existingCustomer = await this.connection.getRepository(ctx, Customer).findOne({
         const existingCustomer = await this.connection.getRepository(ctx, Customer).findOne({
@@ -183,7 +185,7 @@ export class CustomerService {
             return this.connection.getRepository(Customer).save(updatedCustomer);
             return this.connection.getRepository(Customer).save(updatedCustomer);
         } else if (existingCustomer || existingUser) {
         } else if (existingCustomer || existingUser) {
             // Not sure when this situation would occur
             // Not sure when this situation would occur
-            throw new UserInputError(`error.email-address-must-be-unique`);
+            return new EmailAddressConflictAdminError();
         }
         }
         customer.user = await this.userService.createCustomerUser(ctx, input.emailAddress, password);
         customer.user = await this.userService.createCustomerUser(ctx, input.emailAddress, password);
 
 
@@ -192,7 +194,9 @@ export class CustomerService {
             if (verificationToken) {
             if (verificationToken) {
                 const result = await this.userService.verifyUserByToken(ctx, verificationToken);
                 const result = await this.userService.verifyUserByToken(ctx, verificationToken);
                 if (isGraphQlErrorResult(result)) {
                 if (isGraphQlErrorResult(result)) {
-                    // TODO: what to do with an error result here?
+                    // In theory this should never be reached, so we will just
+                    // throw the result
+                    throw result;
                 } else {
                 } else {
                     customer.user = result;
                     customer.user = result;
                 }
                 }
@@ -225,6 +229,50 @@ export class CustomerService {
         return createdCustomer;
         return createdCustomer;
     }
     }
 
 
+    async update(ctx: RequestContext, input: UpdateCustomerShopInput & { id: ID }): Promise<Customer>;
+    async update(
+        ctx: RequestContext,
+        input: UpdateCustomerInput,
+    ): Promise<ErrorResultUnion<UpdateCustomerResult, Customer>>;
+    async update(
+        ctx: RequestContext,
+        input: UpdateCustomerInput | (UpdateCustomerShopInput & { id: ID }),
+    ): Promise<ErrorResultUnion<UpdateCustomerResult, Customer>> {
+        const hasEmailAddress = (i: any): i is UpdateCustomerInput & { emailAddress: string } =>
+            i.hasOwnProperty('emailAddress');
+
+        if (hasEmailAddress(input)) {
+            const existingCustomerInChannel = await this.connection
+                .getRepository(ctx, Customer)
+                .createQueryBuilder('customer')
+                .leftJoin('customer.channels', 'channel')
+                .where('channel.id = :channelId', { channelId: ctx.channelId })
+                .andWhere('customer.emailAddress = :emailAddress', { emailAddress: input.emailAddress })
+                .andWhere('customer.id != :customerId', { customerId: input.id })
+                .andWhere('customer.deletedAt is null')
+                .getOne();
+
+            if (existingCustomerInChannel) {
+                return new EmailAddressConflictAdminError();
+            }
+        }
+
+        const customer = await this.connection.getEntityOrThrow(ctx, Customer, input.id, {
+            channelId: ctx.channelId,
+        });
+        const updatedCustomer = patchEntity(customer, input);
+        await this.connection.getRepository(ctx, Customer).save(updatedCustomer, { reload: false });
+        await this.historyService.createHistoryEntryForCustomer({
+            customerId: customer.id,
+            ctx,
+            type: HistoryEntryType.CUSTOMER_DETAIL_UPDATED,
+            data: {
+                input,
+            },
+        });
+        return assertFound(this.findOne(ctx, customer.id));
+    }
+
     async registerCustomerAccount(
     async registerCustomerAccount(
         ctx: RequestContext,
         ctx: RequestContext,
         input: RegisterCustomerInput,
         input: RegisterCustomerInput,
@@ -459,23 +507,6 @@ export class CustomerService {
         return true;
         return true;
     }
     }
 
 
-    async update(ctx: RequestContext, input: UpdateCustomerInput): Promise<Customer> {
-        const customer = await this.connection.getEntityOrThrow(ctx, Customer, input.id, {
-            channelId: ctx.channelId,
-        });
-        const updatedCustomer = patchEntity(customer, input);
-        await this.connection.getRepository(ctx, Customer).save(updatedCustomer, { reload: false });
-        await this.historyService.createHistoryEntryForCustomer({
-            customerId: customer.id,
-            ctx,
-            type: HistoryEntryType.CUSTOMER_DETAIL_UPDATED,
-            data: {
-                input,
-            },
-        });
-        return assertFound(this.findOne(ctx, customer.id));
-    }
-
     /**
     /**
      * For guest checkouts, we assume that a matching email address is the same customer.
      * For guest checkouts, we assume that a matching email address is the same customer.
      */
      */

+ 2 - 1
packages/core/src/service/services/history.service.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
+import { UpdateCustomerInput as UpdateCustomerShopInput } from '@vendure/common/lib/generated-shop-types';
 import {
 import {
     HistoryEntryListOptions,
     HistoryEntryListOptions,
     HistoryEntryType,
     HistoryEntryType,
@@ -29,7 +30,7 @@ export type CustomerHistoryEntryData = {
         strategy: string;
         strategy: string;
     };
     };
     [HistoryEntryType.CUSTOMER_DETAIL_UPDATED]: {
     [HistoryEntryType.CUSTOMER_DETAIL_UPDATED]: {
-        input: UpdateCustomerInput;
+        input: UpdateCustomerInput | UpdateCustomerShopInput;
     };
     };
     [HistoryEntryType.CUSTOMER_ADDRESS_CREATED]: {
     [HistoryEntryType.CUSTOMER_ADDRESS_CREATED]: {
         address: string;
         address: string;

+ 97 - 60
packages/core/src/service/services/order.service.ts

@@ -8,8 +8,10 @@ import {
     UpdateOrderItemsResult,
     UpdateOrderItemsResult,
 } from '@vendure/common/lib/generated-shop-types';
 } from '@vendure/common/lib/generated-shop-types';
 import {
 import {
+    AddFulfillmentToOrderResult,
     AddNoteToOrderInput,
     AddNoteToOrderInput,
     CancelOrderInput,
     CancelOrderInput,
+    CancelOrderResult,
     CreateAddressInput,
     CreateAddressInput,
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
@@ -18,6 +20,8 @@ import {
     OrderLineInput,
     OrderLineInput,
     OrderProcessState,
     OrderProcessState,
     RefundOrderInput,
     RefundOrderInput,
+    RefundOrderResult,
+    SettlePaymentResult,
     SettleRefundInput,
     SettleRefundInput,
     ShippingMethodQuote,
     ShippingMethodQuote,
     UpdateOrderNoteInput,
     UpdateOrderNoteInput,
@@ -34,6 +38,19 @@ import {
     InternalServerError,
     InternalServerError,
     UserInputError,
     UserInputError,
 } from '../../common/error/errors';
 } from '../../common/error/errors';
+import {
+    AlreadyRefundedError,
+    CancelActiveOrderError,
+    EmptyOrderLineSelectionError,
+    ItemsAlreadyFulfilledError,
+    MultipleOrderError,
+    NothingToRefundError,
+    PaymentOrderMismatchError,
+    PaymentStateTransitionError,
+    QuantityTooGreatError,
+    RefundOrderStateError,
+    SettlePaymentError,
+} from '../../common/error/generated-graphql-admin-errors';
 import {
 import {
     NegativeQuantityError,
     NegativeQuantityError,
     OrderLimitError,
     OrderLimitError,
@@ -607,7 +624,10 @@ export class OrderService {
         return order;
         return order;
     }
     }
 
 
-    async settlePayment(ctx: RequestContext, paymentId: ID): Promise<Payment> {
+    async settlePayment(
+        ctx: RequestContext,
+        paymentId: ID,
+    ): Promise<ErrorResultUnion<SettlePaymentResult, Payment>> {
         const payment = await this.connection.getEntityOrThrow(ctx, Payment, paymentId, {
         const payment = await this.connection.getEntityOrThrow(ctx, Payment, paymentId, {
             relations: ['order'],
             relations: ['order'],
         });
         });
@@ -619,46 +639,56 @@ export class OrderService {
         if (settlePaymentResult.success) {
         if (settlePaymentResult.success) {
             const fromState = payment.state;
             const fromState = payment.state;
             const toState = 'Settled';
             const toState = 'Settled';
-            await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
+            try {
+                await this.paymentStateMachine.transition(ctx, payment.order, payment, toState);
+            } catch (e) {
+                const transitionError = ctx.translate(e.message, { fromState, toState });
+                return new PaymentStateTransitionError(transitionError, fromState, toState);
+            }
             payment.metadata = { ...payment.metadata, ...settlePaymentResult.metadata };
             payment.metadata = { ...payment.metadata, ...settlePaymentResult.metadata };
             await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
             await this.connection.getRepository(ctx, Payment).save(payment, { reload: false });
             this.eventBus.publish(
             this.eventBus.publish(
                 new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
                 new PaymentStateTransitionEvent(fromState, toState, ctx, payment, payment.order),
             );
             );
             if (payment.amount === payment.order.total) {
             if (payment.amount === payment.order.total) {
-                await this.transitionToState(ctx, payment.order.id, 'PaymentSettled');
+                const orderTransitionResult = await this.transitionToState(
+                    ctx,
+                    payment.order.id,
+                    'PaymentSettled',
+                );
+                if (isGraphQlErrorResult(orderTransitionResult)) {
+                    return orderTransitionResult;
+                }
             }
             }
+        } else {
+            return new SettlePaymentError(settlePaymentResult.errorMessage || '');
         }
         }
         return payment;
         return payment;
     }
     }
 
 
-    async createFulfillment(ctx: RequestContext, input: FulfillOrderInput) {
+    async createFulfillment(
+        ctx: RequestContext,
+        input: FulfillOrderInput,
+    ): Promise<ErrorResultUnion<AddFulfillmentToOrderResult, Fulfillment>> {
         if (
         if (
             !input.lines ||
             !input.lines ||
             input.lines.length === 0 ||
             input.lines.length === 0 ||
             input.lines.reduce((total, line) => total + line.quantity, 0) === 0
             input.lines.reduce((total, line) => total + line.quantity, 0) === 0
         ) {
         ) {
-            throw new UserInputError('error.create-fulfillment-nothing-to-fulfill');
+            return new EmptyOrderLineSelectionError();
         }
         }
-        const { items, orders } = await this.getOrdersAndItemsFromLines(
-            ctx,
-            input.lines,
-            i => !i.fulfillment,
-            'error.create-fulfillment-items-already-fulfilled',
-        );
-
-        for (const order of orders) {
-            if (order.state !== 'PaymentSettled' && order.state !== 'PartiallyDelivered') {
-                throw new IllegalOperationError('error.create-fulfillment-orders-must-be-settled');
-            }
+        const ordersAndItems = await this.getOrdersAndItemsFromLines(ctx, input.lines, i => !i.fulfillment);
+        if (!ordersAndItems) {
+            return new ItemsAlreadyFulfilledError();
         }
         }
+
         const fulfillment = await this.fulfillmentService.create(ctx, {
         const fulfillment = await this.fulfillmentService.create(ctx, {
             trackingCode: input.trackingCode,
             trackingCode: input.trackingCode,
             method: input.method,
             method: input.method,
-            orderItems: items,
+            orderItems: ordersAndItems.items,
         });
         });
 
 
-        for (const order of orders) {
+        for (const order of ordersAndItems.orders) {
             await this.historyService.createHistoryEntryForOrder({
             await this.historyService.createHistoryEntryForOrder({
                 ctx,
                 ctx,
                 orderId: order.id,
                 orderId: order.id,
@@ -691,20 +721,33 @@ export class OrderService {
         const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
         const items = lines.reduce((acc, l) => [...acc, ...l.items], [] as OrderItem[]);
         return unique(items.map(i => i.fulfillment).filter(notNullOrUndefined), 'id');
         return unique(items.map(i => i.fulfillment).filter(notNullOrUndefined), 'id');
     }
     }
-    async cancelOrder(ctx: RequestContext, input: CancelOrderInput): Promise<Order> {
+
+    async cancelOrder(
+        ctx: RequestContext,
+        input: CancelOrderInput,
+    ): Promise<ErrorResultUnion<CancelOrderResult, Order>> {
         let allOrderItemsCancelled = false;
         let allOrderItemsCancelled = false;
-        if (input.lines != null) {
-            allOrderItemsCancelled = await this.cancelOrderByOrderLines(ctx, input, input.lines);
+        const cancelResult =
+            input.lines != null
+                ? await this.cancelOrderByOrderLines(ctx, input, input.lines)
+                : await this.cancelOrderById(ctx, input);
+
+        if (isGraphQlErrorResult(cancelResult)) {
+            return cancelResult;
         } else {
         } else {
-            allOrderItemsCancelled = await this.cancelOrderById(ctx, input);
+            allOrderItemsCancelled = cancelResult;
         }
         }
+
         if (allOrderItemsCancelled) {
         if (allOrderItemsCancelled) {
-            await this.transitionToState(ctx, input.orderId, 'Cancelled');
+            const transitionResult = await this.transitionToState(ctx, input.orderId, 'Cancelled');
+            if (isGraphQlErrorResult(transitionResult)) {
+                return transitionResult;
+            }
         }
         }
         return assertFound(this.findOne(ctx, input.orderId));
         return assertFound(this.findOne(ctx, input.orderId));
     }
     }
 
 
-    private async cancelOrderById(ctx: RequestContext, input: CancelOrderInput): Promise<boolean> {
+    private async cancelOrderById(ctx: RequestContext, input: CancelOrderInput) {
         const order = await this.getOrderOrThrow(ctx, input.orderId);
         const order = await this.getOrderOrThrow(ctx, input.orderId);
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
             return true;
             return true;
@@ -721,27 +764,24 @@ export class OrderService {
         ctx: RequestContext,
         ctx: RequestContext,
         input: CancelOrderInput,
         input: CancelOrderInput,
         lines: OrderLineInput[],
         lines: OrderLineInput[],
-    ): Promise<boolean> {
+    ) {
         if (lines.length === 0 || lines.reduce((total, line) => total + line.quantity, 0) === 0) {
         if (lines.length === 0 || lines.reduce((total, line) => total + line.quantity, 0) === 0) {
-            throw new UserInputError('error.cancel-order-lines-nothing-to-cancel');
+            return new EmptyOrderLineSelectionError();
         }
         }
-        const { items, orders } = await this.getOrdersAndItemsFromLines(
-            ctx,
-            lines,
-            i => !i.cancelled,
-            'error.cancel-order-lines-quantity-too-high',
-        );
-        if (1 < orders.length) {
-            throw new IllegalOperationError('error.order-lines-must-belong-to-same-order');
+        const ordersAndItems = await this.getOrdersAndItemsFromLines(ctx, lines, i => !i.cancelled);
+        if (!ordersAndItems) {
+            return new QuantityTooGreatError();
+        }
+        if (1 < ordersAndItems.orders.length) {
+            return new MultipleOrderError();
         }
         }
+        const { orders, items } = ordersAndItems;
         const order = orders[0];
         const order = orders[0];
         if (!idsAreEqual(order.id, input.orderId)) {
         if (!idsAreEqual(order.id, input.orderId)) {
-            throw new IllegalOperationError('error.order-lines-must-belong-to-same-order');
+            return new MultipleOrderError();
         }
         }
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
         if (order.state === 'AddingItems' || order.state === 'ArrangingPayment') {
-            throw new IllegalOperationError('error.cancel-order-lines-invalid-order-state', {
-                state: order.state,
-            });
+            return new CancelActiveOrderError(order.state);
         }
         }
 
 
         // Perform the cancellation
         // Perform the cancellation
@@ -749,12 +789,9 @@ export class OrderService {
         items.forEach(i => (i.cancelled = true));
         items.forEach(i => (i.cancelled = true));
         await this.connection.getRepository(ctx, OrderItem).save(items, { reload: false });
         await this.connection.getRepository(ctx, OrderItem).save(items, { reload: false });
 
 
-        const orderWithItems = await this.connection.getRepository(ctx, Order).findOne(order.id, {
+        const orderWithItems = await this.connection.getEntityOrThrow(ctx, Order, order.id, {
             relations: ['lines', 'lines.items'],
             relations: ['lines', 'lines.items'],
         });
         });
-        if (!orderWithItems) {
-            throw new InternalServerError('error.could-not-find-order');
-        }
         await this.historyService.createHistoryEntryForOrder({
         await this.historyService.createHistoryEntryForOrder({
             ctx,
             ctx,
             orderId: order.id,
             orderId: order.id,
@@ -767,29 +804,31 @@ export class OrderService {
         return orderItemsAreAllCancelled(orderWithItems);
         return orderItemsAreAllCancelled(orderWithItems);
     }
     }
 
 
-    async refundOrder(ctx: RequestContext, input: RefundOrderInput): Promise<Refund> {
+    async refundOrder(
+        ctx: RequestContext,
+        input: RefundOrderInput,
+    ): Promise<ErrorResultUnion<RefundOrderResult, Refund>> {
         if (
         if (
             (!input.lines ||
             (!input.lines ||
                 input.lines.length === 0 ||
                 input.lines.length === 0 ||
                 input.lines.reduce((total, line) => total + line.quantity, 0) === 0) &&
                 input.lines.reduce((total, line) => total + line.quantity, 0) === 0) &&
             input.shipping === 0
             input.shipping === 0
         ) {
         ) {
-            throw new UserInputError('error.refund-order-lines-nothing-to-refund');
+            return new NothingToRefundError();
         }
         }
-        const { items, orders } = await this.getOrdersAndItemsFromLines(
-            ctx,
-            input.lines,
-            i => !i.cancelled,
-            'error.refund-order-lines-quantity-too-high',
-        );
+        const ordersAndItems = await this.getOrdersAndItemsFromLines(ctx, input.lines, i => !i.cancelled);
+        if (!ordersAndItems) {
+            return new QuantityTooGreatError();
+        }
+        const { orders, items } = ordersAndItems;
         if (1 < orders.length) {
         if (1 < orders.length) {
-            throw new IllegalOperationError('error.order-lines-must-belong-to-same-order');
+            return new MultipleOrderError();
         }
         }
         const payment = await this.connection.getEntityOrThrow(ctx, Payment, input.paymentId, {
         const payment = await this.connection.getEntityOrThrow(ctx, Payment, input.paymentId, {
             relations: ['order'],
             relations: ['order'],
         });
         });
         if (orders && orders.length && !idsAreEqual(payment.order.id, orders[0].id)) {
         if (orders && orders.length && !idsAreEqual(payment.order.id, orders[0].id)) {
-            throw new IllegalOperationError('error.refund-order-payment-lines-mismatch');
+            return new PaymentOrderMismatchError();
         }
         }
         const order = payment.order;
         const order = payment.order;
         if (
         if (
@@ -797,12 +836,11 @@ export class OrderService {
             order.state === 'ArrangingPayment' ||
             order.state === 'ArrangingPayment' ||
             order.state === 'PaymentAuthorized'
             order.state === 'PaymentAuthorized'
         ) {
         ) {
-            throw new IllegalOperationError('error.refund-order-lines-invalid-order-state', {
-                state: order.state,
-            });
+            return new RefundOrderStateError(order.state);
         }
         }
-        if (items.some(i => !!i.refundId)) {
-            throw new IllegalOperationError('error.refund-order-item-already-refunded');
+        const alreadyRefunded = items.find(i => !!i.refundId);
+        if (alreadyRefunded) {
+            return new AlreadyRefundedError(alreadyRefunded.refundId as string);
         }
         }
 
 
         return await this.paymentMethodService.createRefund(ctx, input, order, items, payment);
         return await this.paymentMethodService.createRefund(ctx, input, order, items, payment);
@@ -1029,8 +1067,7 @@ export class OrderService {
         ctx: RequestContext,
         ctx: RequestContext,
         orderLinesInput: OrderLineInput[],
         orderLinesInput: OrderLineInput[],
         itemMatcher: (i: OrderItem) => boolean,
         itemMatcher: (i: OrderItem) => boolean,
-        noMatchesError: string,
-    ): Promise<{ orders: Order[]; items: OrderItem[] }> {
+    ): Promise<{ orders: Order[]; items: OrderItem[] } | false> {
         const orders = new Map<ID, Order>();
         const orders = new Map<ID, Order>();
         const items = new Map<ID, OrderItem>();
         const items = new Map<ID, OrderItem>();
 
 
@@ -1055,7 +1092,7 @@ export class OrderService {
             }
             }
             const matchingItems = line.items.sort((a, b) => (a.id < b.id ? -1 : 1)).filter(itemMatcher);
             const matchingItems = line.items.sort((a, b) => (a.id < b.id ? -1 : 1)).filter(itemMatcher);
             if (matchingItems.length < inputLine.quantity) {
             if (matchingItems.length < inputLine.quantity) {
-                throw new IllegalOperationError(noMatchesError);
+                return false;
             }
             }
             matchingItems.slice(0, inputLine.quantity).forEach(item => {
             matchingItems.slice(0, inputLine.quantity).forEach(item => {
                 items.set(item.id, item);
                 items.set(item.id, item);

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

@@ -11,6 +11,7 @@ import { assertNever } from '@vendure/common/lib/shared-utils';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { UserInputError } from '../../common/error/errors';
+import { RefundStateTransitionError } from '../../common/error/generated-graphql-admin-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
 import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
@@ -108,7 +109,7 @@ export class PaymentMethodService {
         order: Order,
         order: Order,
         items: OrderItem[],
         items: OrderItem[],
         payment: Payment,
         payment: Payment,
-    ): Promise<Refund> {
+    ): Promise<Refund | RefundStateTransitionError> {
         const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, payment.method);
         const { paymentMethod, handler } = await this.getMethodAndHandler(ctx, payment.method);
         const itemAmount = items.reduce((sum, item) => sum + item.unitPriceWithTax, 0);
         const itemAmount = items.reduce((sum, item) => sum + item.unitPriceWithTax, 0);
         const refundAmount = itemAmount + input.shipping + input.adjustment;
         const refundAmount = itemAmount + input.shipping + input.adjustment;
@@ -138,7 +139,11 @@ export class PaymentMethodService {
         refund = await this.connection.getRepository(ctx, Refund).save(refund);
         refund = await this.connection.getRepository(ctx, Refund).save(refund);
         if (createRefundResult) {
         if (createRefundResult) {
             const fromState = refund.state;
             const fromState = refund.state;
-            await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
+            try {
+                await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
+            } catch (e) {
+                return new RefundStateTransitionError(e.message, fromState, createRefundResult.state);
+            }
             await this.connection.getRepository(ctx, Refund).save(refund, { reload: false });
             await this.connection.getRepository(ctx, Refund).save(refund, { reload: false });
             this.eventBus.publish(
             this.eventBus.publish(
                 new RefundStateTransitionEvent(fromState, createRefundResult.state, ctx, refund, order),
                 new RefundStateTransitionEvent(fromState, createRefundResult.state, ctx, refund, order),

+ 5 - 5
packages/core/src/service/services/product.service.ts

@@ -5,6 +5,7 @@ import {
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
     Permission,
     Permission,
+    RemoveOptionGroupFromProductResult,
     RemoveProductsFromChannelInput,
     RemoveProductsFromChannelInput,
     UpdateProductInput,
     UpdateProductInput,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
@@ -12,7 +13,9 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { FindOptionsUtils } from 'typeorm';
 import { FindOptionsUtils } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { ErrorResultUnion } from '../../common/error/error-result';
 import { EntityNotFoundError, ForbiddenError, UserInputError } from '../../common/error/errors';
 import { EntityNotFoundError, ForbiddenError, UserInputError } from '../../common/error/errors';
+import { ProductOptionInUseError } from '../../common/error/generated-graphql-admin-errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -282,17 +285,14 @@ export class ProductService {
         ctx: RequestContext,
         ctx: RequestContext,
         productId: ID,
         productId: ID,
         optionGroupId: ID,
         optionGroupId: ID,
-    ): Promise<Translated<Product>> {
+    ): Promise<ErrorResultUnion<RemoveOptionGroupFromProductResult, Translated<Product>>> {
         const product = await this.getProductWithOptionGroups(ctx, productId);
         const product = await this.getProductWithOptionGroups(ctx, productId);
         const optionGroup = product.optionGroups.find(g => idsAreEqual(g.id, optionGroupId));
         const optionGroup = product.optionGroups.find(g => idsAreEqual(g.id, optionGroupId));
         if (!optionGroup) {
         if (!optionGroup) {
             throw new EntityNotFoundError('ProductOptionGroup', optionGroupId);
             throw new EntityNotFoundError('ProductOptionGroup', optionGroupId);
         }
         }
         if (product.variants.length) {
         if (product.variants.length) {
-            throw new UserInputError('error.cannot-remove-option-group-due-to-variants', {
-                code: optionGroup.code,
-                count: product.variants.length,
-            });
+            return new ProductOptionInUseError(optionGroup.code, product.variants.length);
         }
         }
         product.optionGroups = product.optionGroups.filter(g => g.id !== optionGroupId);
         product.optionGroups = product.optionGroups.filter(g => g.id !== optionGroupId);
 
 

+ 18 - 14
packages/core/src/service/services/promotion.service.ts

@@ -7,17 +7,20 @@ import {
     ConfigurableOperationDefinition,
     ConfigurableOperationDefinition,
     ConfigurableOperationInput,
     ConfigurableOperationInput,
     CreatePromotionInput,
     CreatePromotionInput,
+    CreatePromotionResult,
     DeletionResponse,
     DeletionResponse,
     DeletionResult,
     DeletionResult,
     UpdatePromotionInput,
     UpdatePromotionInput,
+    UpdatePromotionResult,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { omit } from '@vendure/common/lib/omit';
 import { omit } from '@vendure/common/lib/omit';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
-import { JustErrorResults } from '../../common/error/error-result';
+import { ErrorResultUnion, JustErrorResults } from '../../common/error/error-result';
 import { UserInputError } from '../../common/error/errors';
 import { UserInputError } from '../../common/error/errors';
+import { MissingConditionsError } from '../../common/error/generated-graphql-admin-errors';
 import {
 import {
     CouponCodeExpiredError,
     CouponCodeExpiredError,
     CouponCodeInvalidError,
     CouponCodeInvalidError,
@@ -97,7 +100,10 @@ export class PromotionService {
         return this.activePromotions;
         return this.activePromotions;
     }
     }
 
 
-    async createPromotion(ctx: RequestContext, input: CreatePromotionInput): Promise<Promotion> {
+    async createPromotion(
+        ctx: RequestContext,
+        input: CreatePromotionInput,
+    ): Promise<ErrorResultUnion<CreatePromotionResult, Promotion>> {
         const promotion = new Promotion({
         const promotion = new Promotion({
             name: input.name,
             name: input.name,
             enabled: input.enabled,
             enabled: input.enabled,
@@ -109,14 +115,19 @@ export class PromotionService {
             actions: input.actions.map(a => this.parseOperationArgs('action', a)),
             actions: input.actions.map(a => this.parseOperationArgs('action', a)),
             priorityScore: this.calculatePriorityScore(input),
             priorityScore: this.calculatePriorityScore(input),
         });
         });
-        this.validatePromotionConditions(promotion);
+        if (promotion.conditions.length === 0 && !promotion.couponCode) {
+            return new MissingConditionsError();
+        }
         this.channelService.assignToCurrentChannel(promotion, ctx);
         this.channelService.assignToCurrentChannel(promotion, ctx);
         const newPromotion = await this.connection.getRepository(ctx, Promotion).save(promotion);
         const newPromotion = await this.connection.getRepository(ctx, Promotion).save(promotion);
         await this.updatePromotions();
         await this.updatePromotions();
         return assertFound(this.findOne(ctx, newPromotion.id));
         return assertFound(this.findOne(ctx, newPromotion.id));
     }
     }
 
 
-    async updatePromotion(ctx: RequestContext, input: UpdatePromotionInput): Promise<Promotion> {
+    async updatePromotion(
+        ctx: RequestContext,
+        input: UpdatePromotionInput,
+    ): Promise<ErrorResultUnion<UpdatePromotionResult, Promotion>> {
         const promotion = await this.connection.getEntityOrThrow(ctx, Promotion, input.id, {
         const promotion = await this.connection.getEntityOrThrow(ctx, Promotion, input.id, {
             channelId: ctx.channelId,
             channelId: ctx.channelId,
         });
         });
@@ -127,7 +138,9 @@ export class PromotionService {
         if (input.actions) {
         if (input.actions) {
             updatedPromotion.actions = input.actions.map(a => this.parseOperationArgs('action', a));
             updatedPromotion.actions = input.actions.map(a => this.parseOperationArgs('action', a));
         }
         }
-        this.validatePromotionConditions(updatedPromotion);
+        if (promotion.conditions.length === 0 && !promotion.couponCode) {
+            return new MissingConditionsError();
+        }
         promotion.priorityScore = this.calculatePriorityScore(input);
         promotion.priorityScore = this.calculatePriorityScore(input);
         await this.connection.getRepository(ctx, Promotion).save(updatedPromotion, { reload: false });
         await this.connection.getRepository(ctx, Promotion).save(updatedPromotion, { reload: false });
         await this.updatePromotions();
         await this.updatePromotions();
@@ -246,13 +259,4 @@ export class PromotionService {
             where: { enabled: true },
             where: { enabled: true },
         });
         });
     }
     }
-
-    /**
-     * Ensure the Promotion has at least one condition or a couponCode specified.
-     */
-    private validatePromotionConditions(promotion: Promotion) {
-        if (promotion.conditions.length === 0 && !promotion.couponCode) {
-            throw new UserInputError('error.promotion-must-have-conditions-or-coupon-code');
-        }
-    }
 }
 }

+ 1 - 1
packages/core/src/service/services/tax-category.service.ts

@@ -50,7 +50,7 @@ export class TaxCategoryService {
             .count({ where: { category: id } });
             .count({ where: { category: id } });
 
 
         if (0 < dependentRates) {
         if (0 < dependentRates) {
-            const message = ctx.translate('error.cannot-remove-tax-category-due-to-tax-rates', {
+            const message = ctx.translate('message.cannot-remove-tax-category-due-to-tax-rates', {
                 name: taxCategory.name,
                 name: taxCategory.name,
                 count: dependentRates,
                 count: dependentRates,
             });
             });

+ 20 - 0
packages/dev-server/dev-config.ts

@@ -9,13 +9,32 @@ import {
     examplePaymentHandler,
     examplePaymentHandler,
     LanguageCode,
     LanguageCode,
     LogLevel,
     LogLevel,
+    PluginCommonModule,
+    ProductService,
     VendureConfig,
     VendureConfig,
+    VendurePlugin,
 } from '@vendure/core';
 } from '@vendure/core';
 import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
 import { ElasticsearchPlugin } from '@vendure/elasticsearch-plugin';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import path from 'path';
 import path from 'path';
 import { ConnectionOptions } from 'typeorm';
 import { ConnectionOptions } from 'typeorm';
 
 
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [
+        {
+            provide: ProductService,
+            useClass: class MyProdService {
+                findOne() {
+                    console.log('YOLO');
+                    return {};
+                }
+            },
+        },
+    ],
+})
+class MyPlugin {}
+
 /**
 /**
  * Config settings used during development
  * Config settings used during development
  */
  */
@@ -58,6 +77,7 @@ export const devConfig: VendureConfig = {
         importAssetsDir: path.join(__dirname, 'import-assets'),
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
     },
     plugins: [
     plugins: [
+        MyPlugin,
         AssetServerPlugin.init({
         AssetServerPlugin.init({
             route: 'assets',
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'assets'),
             assetUploadDir: path.join(__dirname, 'assets'),

+ 231 - 22
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -16,6 +16,11 @@ export type Scalars = {
     Upload: any;
     Upload: any;
 };
 };
 
 
+export type AddFulfillmentToOrderResult =
+    | Fulfillment
+    | EmptyOrderLineSelectionError
+    | ItemsAlreadyFulfilledError;
+
 export type AddNoteToCustomerInput = {
 export type AddNoteToCustomerInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     note: Scalars['String'];
     note: Scalars['String'];
@@ -102,6 +107,13 @@ export type AdministratorSortParameter = {
     emailAddress?: Maybe<SortOrder>;
     emailAddress?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund an OrderItem which has already been refunded */
+export type AlreadyRefundedError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    refundId: Scalars['ID'];
+};
+
 export type Asset = Node & {
 export type Asset = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -178,6 +190,8 @@ export type AuthenticationMethod = Node & {
     strategy: Scalars['String'];
     strategy: Scalars['String'];
 };
 };
 
 
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+
 export type BooleanCustomFieldConfig = CustomField & {
 export type BooleanCustomFieldConfig = CustomField & {
     name: Scalars['String'];
     name: Scalars['String'];
     type: Scalars['String'];
     type: Scalars['String'];
@@ -192,6 +206,13 @@ export type BooleanOperators = {
     eq?: Maybe<Scalars['Boolean']>;
     eq?: Maybe<Scalars['Boolean']>;
 };
 };
 
 
+/** Returned if an attempting to cancel lines from an Order which is still active */
+export type CancelActiveOrderError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    orderState: Scalars['String'];
+};
+
 export type Cancellation = Node &
 export type Cancellation = Node &
     StockMovement & {
     StockMovement & {
         id: Scalars['ID'];
         id: Scalars['ID'];
@@ -211,6 +232,14 @@ export type CancelOrderInput = {
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type CancelOrderResult =
+    | Order
+    | EmptyOrderLineSelectionError
+    | QuantityTooGreatError
+    | MultipleOrderError
+    | CancelActiveOrderError
+    | OrderStateTransitionError;
+
 export type Channel = Node & {
 export type Channel = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -224,6 +253,17 @@ export type Channel = Node & {
     pricesIncludeTax: Scalars['Boolean'];
     pricesIncludeTax: Scalars['Boolean'];
 };
 };
 
 
+/**
+ * Returned when the default LanguageCode of a Channel is no longer found in the `availableLanguages`
+ * of the GlobalSettings
+ */
+export type ChannelDefaultLanguageError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    language: Scalars['String'];
+    channelCode: Scalars['String'];
+};
+
 export type Collection = Node & {
 export type Collection = Node & {
     isPrivate: Scalars['Boolean'];
     isPrivate: Scalars['Boolean'];
     id: Scalars['ID'];
     id: Scalars['ID'];
@@ -436,6 +476,8 @@ export type CreateChannelInput = {
     defaultShippingZoneId: Scalars['ID'];
     defaultShippingZoneId: Scalars['ID'];
 };
 };
 
 
+export type CreateChannelResult = Channel | LanguageNotAvailableError;
+
 export type CreateCollectionInput = {
 export type CreateCollectionInput = {
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
     featuredAssetId?: Maybe<Scalars['ID']>;
     featuredAssetId?: Maybe<Scalars['ID']>;
@@ -474,6 +516,8 @@ export type CreateCustomerInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type CreateCustomerResult = Customer | EmailAddressConflictError;
+
 export type CreateFacetInput = {
 export type CreateFacetInput = {
     code: Scalars['String'];
     code: Scalars['String'];
     isPrivate: Scalars['Boolean'];
     isPrivate: Scalars['Boolean'];
@@ -553,6 +597,8 @@ export type CreatePromotionInput = {
     actions: Array<ConfigurableOperationInput>;
     actions: Array<ConfigurableOperationInput>;
 };
 };
 
 
+export type CreatePromotionResult = Promotion | MissingConditionsError;
+
 export type CreateRoleInput = {
 export type CreateRoleInput = {
     code: Scalars['String'];
     code: Scalars['String'];
     description: Scalars['String'];
     description: Scalars['String'];
@@ -1093,10 +1139,42 @@ export enum DeletionResult {
     NOT_DELETED = 'NOT_DELETED',
     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 & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned if no OrderLines have been specified for the operation */
+export type EmptyOrderLineSelectionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export enum ErrorCode {
 export enum ErrorCode {
     UNKNOWN_ERROR = 'UNKNOWN_ERROR',
     UNKNOWN_ERROR = 'UNKNOWN_ERROR',
     MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
     MIME_TYPE_ERROR = 'MIME_TYPE_ERROR',
+    INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS_ERROR',
+    NATIVE_AUTH_STRATEGY_ERROR = 'NATIVE_AUTH_STRATEGY_ERROR',
+    LANGUAGE_NOT_AVAILABLE_ERROR = 'LANGUAGE_NOT_AVAILABLE_ERROR',
+    EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR',
+    CHANNEL_DEFAULT_LANGUAGE_ERROR = 'CHANNEL_DEFAULT_LANGUAGE_ERROR',
+    SETTLE_PAYMENT_ERROR = 'SETTLE_PAYMENT_ERROR',
+    PAYMENT_STATE_TRANSITION_ERROR = 'PAYMENT_STATE_TRANSITION_ERROR',
     ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
     ORDER_STATE_TRANSITION_ERROR = 'ORDER_STATE_TRANSITION_ERROR',
+    EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR',
+    ITEMS_ALREADY_FULFILLED_ERROR = 'ITEMS_ALREADY_FULFILLED_ERROR',
+    QUANTITY_TOO_GREAT_ERROR = 'QUANTITY_TOO_GREAT_ERROR',
+    MULTIPLE_ORDER_ERROR = 'MULTIPLE_ORDER_ERROR',
+    CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
+    NOTHING_TO_REFUND_ERROR = 'NOTHING_TO_REFUND_ERROR',
+    PAYMENT_ORDER_MISMATCH_ERROR = 'PAYMENT_ORDER_MISMATCH_ERROR',
+    REFUND_ORDER_STATE_ERROR = 'REFUND_ORDER_STATE_ERROR',
+    ALREADY_REFUNDED_ERROR = 'ALREADY_REFUNDED_ERROR',
+    REFUND_STATE_TRANSITION_ERROR = 'REFUND_STATE_TRANSITION_ERROR',
+    FULFILLMENT_STATE_TRANSITION_ERROR = 'FULFILLMENT_STATE_TRANSITION_ERROR',
+    PRODUCT_OPTION_IN_USE_ERROR = 'PRODUCT_OPTION_IN_USE_ERROR',
+    MISSING_CONDITIONS_ERROR = 'MISSING_CONDITIONS_ERROR',
 }
 }
 
 
 export type ErrorResult = {
 export type ErrorResult = {
@@ -1221,6 +1299,15 @@ export type Fulfillment = Node & {
     trackingCode?: Maybe<Scalars['String']>;
     trackingCode?: Maybe<Scalars['String']>;
 };
 };
 
 
+/** Returned when there is an error in transitioning the Fulfillment state */
+export type FulfillmentStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 export type FulfillOrderInput = {
 export type FulfillOrderInput = {
     lines: Array<OrderLineInput>;
     lines: Array<OrderLineInput>;
     method: Scalars['String'];
     method: Scalars['String'];
@@ -1317,6 +1404,18 @@ export type IntCustomFieldConfig = CustomField & {
     step?: Maybe<Scalars['Int']>;
     step?: Maybe<Scalars['Int']>;
 };
 };
 
 
+/** Returned if the user authentication credentials are not valid */
+export type InvalidCredentialsError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned if the specified items are already part of a Fulfillment */
+export type ItemsAlreadyFulfilledError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Job = Node & {
 export type Job = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -1710,6 +1809,12 @@ export enum LanguageCode {
     zu = 'zu',
     zu = 'zu',
 }
 }
 
 
+export type LanguageNotAvailableError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    languageCode: Scalars['String'];
+};
+
 export type LocaleStringCustomFieldConfig = CustomField & {
 export type LocaleStringCustomFieldConfig = CustomField & {
     name: Scalars['String'];
     name: Scalars['String'];
     type: Scalars['String'];
     type: Scalars['String'];
@@ -1732,10 +1837,6 @@ export enum LogicalOperator {
     OR = 'OR',
     OR = 'OR',
 }
 }
 
 
-export type LoginResult = {
-    user: CurrentUser;
-};
-
 export type MimeTypeError = ErrorResult & {
 export type MimeTypeError = ErrorResult & {
     code: ErrorCode;
     code: ErrorCode;
     message: Scalars['String'];
     message: Scalars['String'];
@@ -1743,12 +1844,24 @@ export type MimeTypeError = ErrorResult & {
     mimeType: Scalars['String'];
     mimeType: Scalars['String'];
 };
 };
 
 
+/** Returned if a PromotionCondition has neither a couponCode nor any conditions set */
+export type MissingConditionsError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type MoveCollectionInput = {
 export type MoveCollectionInput = {
     collectionId: Scalars['ID'];
     collectionId: Scalars['ID'];
     parentId: Scalars['ID'];
     parentId: Scalars['ID'];
     index: Scalars['Int'];
     index: Scalars['Int'];
 };
 };
 
 
+/** Returned if an operation has specified OrderLines from multiple Orders */
+export type MultipleOrderError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Mutation = {
 export type Mutation = {
     /** Create a new Administrator */
     /** Create a new Administrator */
     createAdministrator: Administrator;
     createAdministrator: Administrator;
@@ -1770,14 +1883,14 @@ export type Mutation = {
      * Authenticates the user using the native authentication strategy. This mutation
      * Authenticates the user using the native authentication strategy. This mutation
      * is an alias for `authenticate({ native: { ... }})`
      * is an alias for `authenticate({ native: { ... }})`
      */
      */
-    login: LoginResult;
+    login: NativeAuthenticationResult;
     /** Authenticates the user using a named authentication strategy */
     /** Authenticates the user using a named authentication strategy */
-    authenticate: LoginResult;
-    logout: Scalars['Boolean'];
+    authenticate: AuthenticationResult;
+    logout: Success;
     /** Create a new Channel */
     /** Create a new Channel */
-    createChannel: Channel;
+    createChannel: CreateChannelResult;
     /** Update an existing Channel */
     /** Update an existing Channel */
-    updateChannel: Channel;
+    updateChannel: UpdateChannelResult;
     /** Delete a Channel */
     /** Delete a Channel */
     deleteChannel: DeletionResponse;
     deleteChannel: DeletionResponse;
     /** Create a new Collection */
     /** Create a new Collection */
@@ -1805,9 +1918,9 @@ export type Mutation = {
     /** Remove Customers from a CustomerGroup */
     /** Remove Customers from a CustomerGroup */
     removeCustomersFromGroup: CustomerGroup;
     removeCustomersFromGroup: CustomerGroup;
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
     /** Create a new Customer. If a password is provided, a new User will also be created an linked to the Customer. */
-    createCustomer: Customer;
+    createCustomer: CreateCustomerResult;
     /** Update an existing Customer */
     /** Update an existing Customer */
-    updateCustomer: Customer;
+    updateCustomer: UpdateCustomerResult;
     /** Delete a Customer */
     /** Delete a Customer */
     deleteCustomer: DeletionResponse;
     deleteCustomer: DeletionResponse;
     /** Create a new Address and associate it with the Customer specified by customerId */
     /** Create a new Address and associate it with the Customer specified by customerId */
@@ -1831,20 +1944,20 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
     deleteFacetValues: Array<DeletionResponse>;
-    updateGlobalSettings: GlobalSettings;
+    updateGlobalSettings: UpdateGlobalSettingsResult;
     importProducts?: Maybe<ImportInfo>;
     importProducts?: Maybe<ImportInfo>;
     /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
     /** Remove all settled jobs in the given queues olfer than the given date. Returns the number of jobs deleted. */
     removeSettledJobs: Scalars['Int'];
     removeSettledJobs: Scalars['Int'];
-    settlePayment: Payment;
-    fulfillOrder: Fulfillment;
-    cancelOrder: Order;
-    refundOrder: Refund;
-    settleRefund: Refund;
+    settlePayment: SettlePaymentResult;
+    addFulfillmentToOrder: AddFulfillmentToOrderResult;
+    cancelOrder: CancelOrderResult;
+    refundOrder: RefundOrderResult;
+    settleRefund: SettleRefundResult;
     addNoteToOrder: Order;
     addNoteToOrder: Order;
     updateOrderNote: HistoryEntry;
     updateOrderNote: HistoryEntry;
     deleteOrderNote: DeletionResponse;
     deleteOrderNote: DeletionResponse;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
     transitionOrderToState?: Maybe<TransitionOrderToStateResult>;
-    transitionFulfillmentToState: Fulfillment;
+    transitionFulfillmentToState: TransitionFulfillmentToStateResult;
     setOrderCustomFields?: Maybe<Order>;
     setOrderCustomFields?: Maybe<Order>;
     /** Update an existing PaymentMethod */
     /** Update an existing PaymentMethod */
     updatePaymentMethod: PaymentMethod;
     updatePaymentMethod: PaymentMethod;
@@ -1866,7 +1979,7 @@ export type Mutation = {
     /** Add an OptionGroup to a Product */
     /** Add an OptionGroup to a Product */
     addOptionGroupToProduct: Product;
     addOptionGroupToProduct: Product;
     /** Remove an OptionGroup from a Product */
     /** Remove an OptionGroup from a Product */
-    removeOptionGroupFromProduct: Product;
+    removeOptionGroupFromProduct: RemoveOptionGroupFromProductResult;
     /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
     /** Create a set of ProductVariants based on the OptionGroups assigned to the given Product */
     createProductVariants: Array<Maybe<ProductVariant>>;
     createProductVariants: Array<Maybe<ProductVariant>>;
     /** Update existing ProductVariants */
     /** Update existing ProductVariants */
@@ -1877,8 +1990,8 @@ export type Mutation = {
     assignProductsToChannel: Array<Product>;
     assignProductsToChannel: Array<Product>;
     /** Removes Products from the specified Channel */
     /** Removes Products from the specified Channel */
     removeProductsFromChannel: Array<Product>;
     removeProductsFromChannel: Array<Product>;
-    createPromotion: Promotion;
-    updatePromotion: Promotion;
+    createPromotion: CreatePromotionResult;
+    updatePromotion: UpdatePromotionResult;
     deletePromotion: DeletionResponse;
     deletePromotion: DeletionResponse;
     /** Create a new Role */
     /** Create a new Role */
     createRole: Role;
     createRole: Role;
@@ -2105,7 +2218,7 @@ export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
-export type MutationFulfillOrderArgs = {
+export type MutationAddFulfillmentToOrderArgs = {
     input: FulfillOrderInput;
     input: FulfillOrderInput;
 };
 };
 
 
@@ -2291,15 +2404,29 @@ export type MutationRemoveMembersFromZoneArgs = {
     memberIds: Array<Scalars['ID']>;
     memberIds: Array<Scalars['ID']>;
 };
 };
 
 
+export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+
 export type NativeAuthInput = {
 export type NativeAuthInput = {
     username: Scalars['String'];
     username: Scalars['String'];
     password: Scalars['String'];
     password: Scalars['String'];
 };
 };
 
 
+/** Retured when attempting an operation that relies on the NativeAuthStrategy, if that strategy is not configured. */
+export type NativeAuthStrategyError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Node = {
 export type Node = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
+/** Returned if an attempting to refund an Order but neither items nor shipping refund was specified */
+export type NothingToRefundError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type NumberOperators = {
 export type NumberOperators = {
     eq?: Maybe<Scalars['Float']>;
     eq?: Maybe<Scalars['Float']>;
     lt?: Maybe<Scalars['Float']>;
     lt?: Maybe<Scalars['Float']>;
@@ -2509,6 +2636,21 @@ export type PaymentMethodSortParameter = {
     code?: Maybe<SortOrder>;
     code?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if an attempting to refund a Payment against OrderLines from a different Order */
+export type PaymentOrderMismatchError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Payment state */
+export type PaymentStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
 /**
 /**
  * "
  * "
  * @description
  * @description
@@ -2640,6 +2782,13 @@ export type ProductOptionGroupTranslationInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type ProductOptionInUseError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    optionGroupCode: Scalars['String'];
+    productVariantCount: Scalars['Int'];
+};
+
 export type ProductOptionTranslation = {
 export type ProductOptionTranslation = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -2816,6 +2965,12 @@ export type PromotionSortParameter = {
     name?: Maybe<SortOrder>;
     name?: Maybe<SortOrder>;
 };
 };
 
 
+/** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
+export type QuantityTooGreatError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type Query = {
 export type Query = {
     administrators: AdministratorList;
     administrators: AdministratorList;
     administrator?: Maybe<Administrator>;
     administrator?: Maybe<Administrator>;
@@ -3063,6 +3218,35 @@ export type RefundOrderInput = {
     reason?: Maybe<Scalars['String']>;
     reason?: Maybe<Scalars['String']>;
 };
 };
 
 
+export type RefundOrderResult =
+    | Refund
+    | QuantityTooGreatError
+    | NothingToRefundError
+    | OrderStateTransitionError
+    | MultipleOrderError
+    | PaymentOrderMismatchError
+    | RefundOrderStateError
+    | AlreadyRefundedError
+    | RefundStateTransitionError;
+
+/** Returned if an attempting to refund an Order which is not in the expected state */
+export type RefundOrderStateError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    orderState: Scalars['String'];
+};
+
+/** Returned when there is an error in transitioning the Refund state */
+export type RefundStateTransitionError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    transitionError: Scalars['String'];
+    fromState: Scalars['String'];
+    toState: Scalars['String'];
+};
+
+export type RemoveOptionGroupFromProductResult = Product | ProductOptionInUseError;
+
 export type RemoveProductsFromChannelInput = {
 export type RemoveProductsFromChannelInput = {
     productIds: Array<Scalars['ID']>;
     productIds: Array<Scalars['ID']>;
     channelId: Scalars['ID'];
     channelId: Scalars['ID'];
@@ -3197,11 +3381,26 @@ export type ServerConfig = {
     customFieldConfig: CustomFields;
     customFieldConfig: CustomFields;
 };
 };
 
 
+/** Returned if the Payment settlement fails */
+export type SettlePaymentError = ErrorResult & {
+    code: ErrorCode;
+    message: Scalars['String'];
+    paymentErrorMessage: Scalars['String'];
+};
+
+export type SettlePaymentResult =
+    | Payment
+    | SettlePaymentError
+    | PaymentStateTransitionError
+    | OrderStateTransitionError;
+
 export type SettleRefundInput = {
 export type SettleRefundInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     transactionId: Scalars['String'];
     transactionId: Scalars['String'];
 };
 };
 
 
+export type SettleRefundResult = Refund | RefundStateTransitionError;
+
 export type ShippingMethod = Node & {
 export type ShippingMethod = Node & {
     id: Scalars['ID'];
     id: Scalars['ID'];
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
@@ -3401,6 +3600,8 @@ export type TestShippingMethodResult = {
     quote?: Maybe<TestShippingMethodQuote>;
     quote?: Maybe<TestShippingMethodQuote>;
 };
 };
 
 
+export type TransitionFulfillmentToStateResult = Fulfillment | FulfillmentStateTransitionError;
+
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 export type TransitionOrderToStateResult = Order | OrderStateTransitionError;
 
 
 export type UpdateAddressInput = {
 export type UpdateAddressInput = {
@@ -3445,6 +3646,8 @@ export type UpdateChannelInput = {
     defaultShippingZoneId?: Maybe<Scalars['ID']>;
     defaultShippingZoneId?: Maybe<Scalars['ID']>;
 };
 };
 
 
+export type UpdateChannelResult = Channel | LanguageNotAvailableError;
+
 export type UpdateCollectionInput = {
 export type UpdateCollectionInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3492,6 +3695,8 @@ export type UpdateCustomerNoteInput = {
     note: Scalars['String'];
     note: Scalars['String'];
 };
 };
 
 
+export type UpdateCustomerResult = Customer | EmailAddressConflictError;
+
 export type UpdateFacetInput = {
 export type UpdateFacetInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     isPrivate?: Maybe<Scalars['Boolean']>;
     isPrivate?: Maybe<Scalars['Boolean']>;
@@ -3513,6 +3718,8 @@ export type UpdateGlobalSettingsInput = {
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
 };
 };
 
 
+export type UpdateGlobalSettingsResult = GlobalSettings | ChannelDefaultLanguageError;
+
 export type UpdateOrderInput = {
 export type UpdateOrderInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
@@ -3582,6 +3789,8 @@ export type UpdatePromotionInput = {
     actions?: Maybe<Array<ConfigurableOperationInput>>;
     actions?: Maybe<Array<ConfigurableOperationInput>>;
 };
 };
 
 
+export type UpdatePromotionResult = Promotion | MissingConditionsError;
+
 export type UpdateRoleInput = {
 export type UpdateRoleInput = {
     id: Scalars['ID'];
     id: Scalars['ID'];
     code?: Maybe<Scalars['String']>;
     code?: Maybe<Scalars['String']>;

+ 5 - 5
packages/elasticsearch-plugin/src/elasticsearch.service.ts

@@ -312,21 +312,21 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
 
 
     private getSearchResultAssets(
     private getSearchResultAssets(
         source: ProductIndexItem | VariantIndexItem,
         source: ProductIndexItem | VariantIndexItem,
-    ): { productAsset: SearchResultAsset | null; productVariantAsset: SearchResultAsset | null } {
-        const productAsset: SearchResultAsset | null = source.productAssetId
+    ): { productAsset: SearchResultAsset | undefined; productVariantAsset: SearchResultAsset | undefined } {
+        const productAsset: SearchResultAsset | undefined = source.productAssetId
             ? {
             ? {
                   id: source.productAssetId.toString(),
                   id: source.productAssetId.toString(),
                   preview: source.productPreview,
                   preview: source.productPreview,
                   focalPoint: source.productPreviewFocalPoint,
                   focalPoint: source.productPreviewFocalPoint,
               }
               }
-            : null;
-        const productVariantAsset: SearchResultAsset | null = source.productVariantAssetId
+            : undefined;
+        const productVariantAsset: SearchResultAsset | undefined = source.productVariantAssetId
             ? {
             ? {
                   id: source.productVariantAssetId.toString(),
                   id: source.productVariantAssetId.toString(),
                   preview: source.productVariantPreview,
                   preview: source.productVariantPreview,
                   focalPoint: source.productVariantPreviewFocalPoint,
                   focalPoint: source.productVariantPreviewFocalPoint,
               }
               }
-            : null;
+            : undefined;
         return { productAsset, productVariantAsset };
         return { productAsset, productVariantAsset };
     }
     }
 
 

+ 8 - 8
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -637,13 +637,13 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             slug: v.product.slug,
             slug: v.product.slug,
             productId: v.product.id,
             productId: v.product.id,
             productName: v.product.name,
             productName: v.product.name,
-            productAssetId: productAsset ? productAsset.id : null,
+            productAssetId: productAsset ? productAsset.id : undefined,
             productPreview: productAsset ? productAsset.preview : '',
             productPreview: productAsset ? productAsset.preview : '',
-            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || null : null,
+            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
             productVariantName: v.name,
             productVariantName: v.name,
-            productVariantAssetId: variantAsset ? variantAsset.id : null,
+            productVariantAssetId: variantAsset ? variantAsset.id : undefined,
             productVariantPreview: variantAsset ? variantAsset.preview : '',
             productVariantPreview: variantAsset ? variantAsset.preview : '',
-            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || null : null,
+            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
             price: v.price,
             price: v.price,
             priceWithTax: v.priceWithTax,
             priceWithTax: v.priceWithTax,
             currencyCode: v.currencyCode,
             currencyCode: v.currencyCode,
@@ -676,14 +676,14 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
             slug: first.product.slug,
             slug: first.product.slug,
             productId: first.product.id,
             productId: first.product.id,
             productName: first.product.name,
             productName: first.product.name,
-            productAssetId: productAsset ? productAsset.id : null,
+            productAssetId: productAsset ? productAsset.id : undefined,
             productPreview: productAsset ? productAsset.preview : '',
             productPreview: productAsset ? productAsset.preview : '',
-            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || null : null,
+            productPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
             productVariantId: first.id,
             productVariantId: first.id,
             productVariantName: first.name,
             productVariantName: first.name,
-            productVariantAssetId: variantAsset ? variantAsset.id : null,
+            productVariantAssetId: variantAsset ? variantAsset.id : undefined,
             productVariantPreview: variantAsset ? variantAsset.preview : '',
             productVariantPreview: variantAsset ? variantAsset.preview : '',
-            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || null : null,
+            productVariantPreviewFocalPoint: productAsset ? productAsset.focalPoint || undefined : undefined,
             priceMin: Math.min(...prices),
             priceMin: Math.min(...prices),
             priceMax: Math.max(...prices),
             priceMax: Math.max(...prices),
             priceWithTaxMin: Math.min(...pricesWithTax),
             priceWithTaxMin: Math.min(...pricesWithTax),

+ 5 - 5
packages/elasticsearch-plugin/src/types.ts

@@ -31,12 +31,12 @@ export type PriceRangeBucket = {
 };
 };
 
 
 export type IndexItemAssets = {
 export type IndexItemAssets = {
-    productAssetId: ID | null;
+    productAssetId: ID | undefined;
     productPreview: string;
     productPreview: string;
-    productPreviewFocalPoint: Coordinate | null;
-    productVariantAssetId: ID | null;
+    productPreviewFocalPoint: Coordinate | undefined;
+    productVariantAssetId: ID | undefined;
     productVariantPreview: string;
     productVariantPreview: string;
-    productVariantPreviewFocalPoint: Coordinate | null;
+    productVariantPreviewFocalPoint: Coordinate | undefined;
 };
 };
 
 
 export type VariantIndexItem = Omit<
 export type VariantIndexItem = Omit<
@@ -214,7 +214,7 @@ export class DeleteAssetMessage extends WorkerMessage<UpdateAssetMessageData, bo
     static readonly pattern = 'DeleteAsset';
     static readonly pattern = 'DeleteAsset';
 }
 }
 
 
-type Maybe<T> = T | null | undefined;
+type Maybe<T> = T | undefined;
 type CustomMappingDefinition<Args extends any[], T extends string, R> = {
 type CustomMappingDefinition<Args extends any[], T extends string, R> = {
     graphQlType: T;
     graphQlType: T;
     valueFn: (...args: Args) => R;
     valueFn: (...args: Args) => R;

+ 8 - 3
packages/testing/src/data-population/mock-data.service.ts

@@ -23,8 +23,10 @@ export class MockDataService {
             const query1 = gql`
             const query1 = gql`
                 mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
                 mutation CreateCustomer($input: CreateCustomerInput!, $password: String) {
                     createCustomer(input: $input, password: $password) {
                     createCustomer(input: $input, password: $password) {
-                        id
-                        emailAddress
+                        ... on Customer {
+                            id
+                            emailAddress
+                        }
                     }
                     }
                 }
                 }
             `;
             `;
@@ -41,7 +43,10 @@ export class MockDataService {
 
 
             const customer: { id: string; emailAddress: string } | void = await this.client
             const customer: { id: string; emailAddress: string } | void = await this.client
                 .query(query1, variables1)
                 .query(query1, variables1)
-                .then((data: any) => data.createCustomer, err => this.log(err));
+                .then(
+                    (data: any) => data.createCustomer,
+                    err => this.log(err),
+                );
 
 
             if (customer) {
             if (customer) {
                 const query2 = gql`
                 const query2 = gql`

+ 13 - 5
packages/testing/src/simple-graphql-client.ts

@@ -14,13 +14,17 @@ import { createUploadPostData } from './utils/create-upload-post-data';
 const LOGIN = gql`
 const LOGIN = gql`
     mutation($username: String!, $password: String!) {
     mutation($username: String!, $password: String!) {
         login(username: $username, password: $password) {
         login(username: $username, password: $password) {
-            user {
+            ... on CurrentUser {
                 id
                 id
                 identifier
                 identifier
                 channels {
                 channels {
                     token
                     token
                 }
                 }
             }
             }
+            ... on ErrorResult {
+                code
+                message
+            }
         }
         }
     }
     }
 `;
 `;
@@ -129,14 +133,16 @@ export class SimpleGraphQLClient {
             await this.query(
             await this.query(
                 gql`
                 gql`
                     mutation {
                     mutation {
-                        logout
+                        logout {
+                            success
+                        }
                     }
                     }
                 `,
                 `,
             );
             );
         }
         }
         const result = await this.query(LOGIN, { username, password });
         const result = await this.query(LOGIN, { username, password });
-        if (result.login.user.channels.length === 1) {
-            this.setChannelToken(result.login.user.channels[0].token);
+        if (result.login.channels?.length === 1) {
+            this.setChannelToken(result.login.channels[0].token);
         }
         }
         return result.login;
         return result.login;
     }
     }
@@ -161,7 +167,9 @@ export class SimpleGraphQLClient {
         await this.query(
         await this.query(
             gql`
             gql`
                 mutation {
                 mutation {
-                    logout
+                    logout {
+                        success
+                    }
                 }
                 }
             `,
             `,
         );
         );

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


+ 15 - 11
scripts/codegen/plugins/graphql-errors-plugin.ts

@@ -20,6 +20,7 @@ import {
     UnionTypeDefinitionNode,
     UnionTypeDefinitionNode,
     visit,
     visit,
     Visitor,
     Visitor,
+    ListTypeNode,
 } from 'graphql';
 } from 'graphql';
 
 
 // This plugin generates classes for all GraphQL types which implement the `ErrorResult` interface.
 // This plugin generates classes for all GraphQL types which implement the `ErrorResult` interface.
@@ -32,11 +33,17 @@ export const ERROR_INTERFACE_NAME = 'ErrorResult';
 const empty = () => '';
 const empty = () => '';
 
 
 const errorsVisitor: Visitor<any> = {
 const errorsVisitor: Visitor<any> = {
-    NonNullType(node: NonNullTypeNode): string {
-        return node.type.kind === 'NamedType' ? node.type.name.value : '';
+    NonNullType(node: NonNullTypeNode): string | ListTypeNode {
+        return node.type.kind === 'NamedType'
+            ? node.type.name.value
+            : node.type.kind === 'ListType'
+            ? node.type
+            : '';
     },
     },
     FieldDefinition(node: FieldDefinitionNode): string {
     FieldDefinition(node: FieldDefinitionNode): string {
-        return `  ${node.name.value}: Scalars['${node.type}']`;
+        const scalarType = node.type.kind === 'ListType' ? node.type.type : node.type;
+        const listPart = node.type.kind === 'ListType' ? `[]` : ``;
+        return `  ${node.name.value}: Scalars['${scalarType}']${listPart}`;
     },
     },
     ScalarTypeDefinition: empty,
     ScalarTypeDefinition: empty,
     InputObjectTypeDefinition: empty,
     InputObjectTypeDefinition: empty,
@@ -49,7 +56,7 @@ const errorsVisitor: Visitor<any> = {
         return [
         return [
             `export class ${ERROR_INTERFACE_NAME} {`,
             `export class ${ERROR_INTERFACE_NAME} {`,
             `  readonly __typename: string;`,
             `  readonly __typename: string;`,
-            `  readonly code: ErrorCode;`,
+            `  readonly code: string;`,
             ...node.fields.filter(f => !(f as any).includes('code:')).map(f => `${f};`),
             ...node.fields.filter(f => !(f as any).includes('code:')).map(f => `${f};`),
             `}`,
             `}`,
         ].join('\n');
         ].join('\n');
@@ -68,7 +75,10 @@ const errorsVisitor: Visitor<any> = {
         return [
         return [
             `export class ${node.name.value} extends ${ERROR_INTERFACE_NAME} {`,
             `export class ${node.name.value} extends ${ERROR_INTERFACE_NAME} {`,
             `  readonly __typename = '${node.name.value}';`,
             `  readonly __typename = '${node.name.value}';`,
-            `  readonly code = ErrorCode.${camelToUpperSnakeCase(node.name.value)};`,
+            // We cast this to "any" otherwise we need to specify it as type "ErrorCode",
+            // which means shared ErrorResult classes e.g. OrderStateTransitionError
+            // will not be compatible between the admin and shop variations.
+            `  readonly code = '${camelToUpperSnakeCase(node.name.value)}' as any;`,
             `  readonly message = '${camelToUpperSnakeCase(node.name.value)}';`,
             `  readonly message = '${camelToUpperSnakeCase(node.name.value)}';`,
             `  constructor(`,
             `  constructor(`,
             ...node.fields
             ...node.fields
@@ -93,7 +103,6 @@ export const plugin: PluginFunction<any> = (schema, documents, config, info) =>
     return {
     return {
         content: [
         content: [
             `/** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */`,
             `/** This file was generated by the graphql-errors-plugin, which is part of the "codegen" npm script. */`,
-            generateErrorCodeImport(schema),
             generateScalars(schema, config),
             generateScalars(schema, config),
             ...defs,
             ...defs,
             defs.length ? generateIsErrorFunction(schema) : '',
             defs.length ? generateIsErrorFunction(schema) : '',
@@ -102,11 +111,6 @@ 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 {
 function generateScalars(schema: GraphQLSchema, config: any): string {
     const scalarMap = buildScalars(schema, config.scalars);
     const scalarMap = buildScalars(schema, config.scalars);
     const allScalars = Object.keys(scalarMap)
     const allScalars = Object.keys(scalarMap)

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