Jelajahi Sumber

feat(server): Improved error handling

All errors now inherit from I18nError and each has a distinct error code, which makes client-side handling consistent. Closes #42
Michael Bromley 7 tahun lalu
induk
melakukan
2e92fc123f
40 mengubah file dengan 200 tambahan dan 151 penghapusan
  1. 9 0
      README.md
  2. 0 0
      schema.json
  3. 9 9
      server/e2e/auth.e2e-spec.ts
  4. 5 1
      server/e2e/order.e2e-spec.ts
  5. 1 1
      server/e2e/product.e2e-spec.ts
  6. 2 2
      server/src/api/common/request-context.service.ts
  7. 7 1
      server/src/api/middleware/auth-guard.ts
  8. 2 2
      server/src/api/resolvers/auth.resolver.ts
  9. 2 2
      server/src/api/resolvers/facet.resolver.ts
  10. 3 3
      server/src/api/resolvers/order.resolver.ts
  11. 2 5
      server/src/api/resolvers/product.resolver.ts
  12. 58 0
      server/src/common/error/errors.ts
  13. 2 2
      server/src/config/asset-preview-strategy/no-asset-preview-strategy.ts
  14. 7 7
      server/src/config/asset-storage-strategy/no-asset-storage-strategy.ts
  15. 2 3
      server/src/entity/payment-method/payment-method.entity.ts
  16. 3 3
      server/src/entity/product-variant/product-variant.subscriber.ts
  17. 10 4
      server/src/i18n/i18n-error.ts
  18. 3 0
      server/src/i18n/i18n.service.ts
  19. 3 2
      server/src/i18n/messages/en.json
  20. 2 2
      server/src/service/helpers/list-query-builder/parse-filter-params.ts
  21. 2 2
      server/src/service/helpers/list-query-builder/parse-sort-params.ts
  22. 5 6
      server/src/service/helpers/order-state-machine/order-state-machine.ts
  23. 2 2
      server/src/service/helpers/translatable-saver/translation-differ.ts
  24. 2 5
      server/src/service/helpers/utils/get-entity-or-throw.ts
  25. 2 2
      server/src/service/helpers/utils/translate-entity.ts
  26. 4 10
      server/src/service/services/administrator.service.ts
  27. 5 5
      server/src/service/services/auth.service.ts
  28. 4 7
      server/src/service/services/channel.service.ts
  29. 3 3
      server/src/service/services/customer.service.ts
  30. 9 12
      server/src/service/services/order.service.ts
  31. 2 2
      server/src/service/services/payment-method.service.ts
  32. 4 7
      server/src/service/services/product-variant.service.ts
  33. 3 6
      server/src/service/services/product.service.ts
  34. 3 6
      server/src/service/services/promotion.service.ts
  35. 5 5
      server/src/service/services/role.service.ts
  36. 4 7
      server/src/service/services/shipping-method.service.ts
  37. 2 5
      server/src/service/services/tax-category.service.ts
  38. 2 5
      server/src/service/services/tax-rate.service.ts
  39. 2 5
      server/yarn.lock
  40. 3 0
      shared/generated-types.ts

+ 9 - 0
README.md

@@ -96,6 +96,15 @@ This can be overridden by appending a `lang` query parameter to the url (e.g. `h
 
 
 All locales in Vendure are represented by 2-character [ISO 639-1 language codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
 All locales in Vendure are represented by 2-character [ISO 639-1 language codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
 
 
+Translations for localized strings are located in the [i18n/messages](./server/src/i18n/messages) directory.
+
+### Errors
+
+All errors thrown by the Vendure server can be found in the [errors.ts file](./server/src/common/error/errors.ts). 
+
+All errors extend from `I18nError`, which means that the error messages are localized as described above. Each error type
+has a distinct code which can be used by the front-end client in order to correctly handle the error.
+
 ### Custom Fields
 ### Custom Fields
 
 
 The developer may add custom fields to most of the entities in Vendure, which may contain any data specific to their
 The developer may add custom fields to most of the entities in Vendure, which may contain any data specific to their

File diff ditekan karena terlalu besar
+ 0 - 0
schema.json


+ 9 - 9
server/e2e/auth.e2e-spec.ts

@@ -217,7 +217,7 @@ describe('Authorization & permissions', () => {
                 await client.asUserWithCredentials(emailAddress, '');
                 await client.asUserWithCredentials(emailAddress, '');
                 fail('should have thrown');
                 fail('should have thrown');
             } catch (err) {
             } catch (err) {
-                expect(getErrorStatusCode(err)).toBe(401);
+                expect(getErrorCode(err)).toBe('UNAUTHORIZED');
             }
             }
         });
         });
 
 
@@ -268,26 +268,26 @@ describe('Authorization & permissions', () => {
             const status = await client.queryStatus(operation, variables);
             const status = await client.queryStatus(operation, variables);
             expect(status).toBe(200);
             expect(status).toBe(200);
         } catch (e) {
         } catch (e) {
-            const status = getErrorStatusCode(e);
-            if (!status) {
+            const errorCode = getErrorCode(e);
+            if (!errorCode) {
                 fail(`Unexpected failure: ${e}`);
                 fail(`Unexpected failure: ${e}`);
             } else {
             } else {
-                fail(`Operation should be allowed, got status ${getErrorStatusCode(e)}`);
+                fail(`Operation should be allowed, got status ${getErrorCode(e)}`);
             }
             }
         }
         }
     }
     }
 
 
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
     async function assertRequestForbidden<V>(operation: DocumentNode, variables: V) {
         try {
         try {
-            const status = await client.queryStatus(operation, variables);
-            fail(`Should have thrown with 403 error, got ${status}`);
+            const status = await client.query(operation, variables);
+            fail(`Should have thrown`);
         } catch (e) {
         } catch (e) {
-            expect(getErrorStatusCode(e)).toBe(403);
+            expect(getErrorCode(e)).toBe('FORBIDDEN');
         }
         }
     }
     }
 
 
-    function getErrorStatusCode(err: any): number {
-        return err.response.errors[0].message.statusCode;
+    function getErrorCode(err: any): string {
+        return err.response.errors[0].extensions.code;
     }
     }
 
 
     async function createAdministratorWithPermissions(
     async function createAdministratorWithPermissions(

+ 5 - 1
server/e2e/order.e2e-spec.ts

@@ -573,7 +573,11 @@ describe('Orders', () => {
                         });
                         });
                         fail('Should have thrown');
                         fail('Should have thrown');
                     } catch (err) {
                     } catch (err) {
-                        expect(err.message).toEqual(expect.stringContaining(`This action is forbidden`));
+                        expect(err.message).toEqual(
+                            expect.stringContaining(
+                                `You are not currently authorized to perform this action`,
+                            ),
+                        );
                     }
                     }
                 });
                 });
             });
             });

+ 1 - 1
server/e2e/product.e2e-spec.ts

@@ -383,7 +383,7 @@ describe('Product resolver', () => {
                 fail('Should have thrown');
                 fail('Should have thrown');
             } catch (err) {
             } catch (err) {
                 expect(err.message).toEqual(
                 expect(err.message).toEqual(
-                    expect.stringContaining(`No OptionGroup with the id '999' could be found`),
+                    expect.stringContaining(`No ProductOptionGroup with the id '999' could be found`),
                 );
                 );
             }
             }
         });
         });

+ 2 - 2
server/src/api/common/request-context.service.ts

@@ -2,13 +2,13 @@ import { Injectable } from '@nestjs/common';
 import { Request } from 'express';
 import { Request } from 'express';
 import { LanguageCode, Permission } from 'shared/generated-types';
 import { LanguageCode, Permission } from 'shared/generated-types';
 
 
+import { NoValidChannelError } from '../../common/error/errors';
 import { idsAreEqual } from '../../common/utils';
 import { idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ChannelService } from '../../service/services/channel.service';
 import { ChannelService } from '../../service/services/channel.service';
 
 
 import { RequestContext } from './request-context';
 import { RequestContext } from './request-context';
@@ -56,7 +56,7 @@ export class RequestContextService {
         } else if (req && req.headers && req.headers[tokenKey]) {
         } else if (req && req.headers && req.headers[tokenKey]) {
             channelToken = req.headers[tokenKey] as string;
             channelToken = req.headers[tokenKey] as string;
         } else {
         } else {
-            throw new I18nError('error.no-valid-channel-specified');
+            throw new NoValidChannelError();
         }
         }
         return channelToken;
         return channelToken;
     }
     }

+ 7 - 1
server/src/api/middleware/auth-guard.ts

@@ -8,6 +8,7 @@ import { ConfigService } from '../../config/config.service';
 import { Session } from '../../entity/session/session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { AuthService } from '../../service/services/auth.service';
 import { AuthService } from '../../service/services/auth.service';
 
 
+import { ForbiddenError } from '../../common/error/errors';
 import { extractAuthToken } from '../common/extract-auth-token';
 import { extractAuthToken } from '../common/extract-auth-token';
 import { REQUEST_CONTEXT_KEY, RequestContextService } from '../common/request-context.service';
 import { REQUEST_CONTEXT_KEY, RequestContextService } from '../common/request-context.service';
 import { setAuthToken } from '../common/set-auth-token';
 import { setAuthToken } from '../common/set-auth-token';
@@ -43,7 +44,12 @@ export class AuthGuard implements CanActivate {
         if (authDisabled || !permissions || isPublic) {
         if (authDisabled || !permissions || isPublic) {
             return true;
             return true;
         } else {
         } else {
-            return requestContext.isAuthorized || requestContext.authorizedAsOwnerOnly;
+            const canActivate = requestContext.isAuthorized || requestContext.authorizedAsOwnerOnly;
+            if (!canActivate) {
+                throw new ForbiddenError();
+            } else {
+                return canActivate;
+            }
         }
         }
     }
     }
 
 

+ 2 - 2
server/src/api/resolvers/auth.resolver.ts

@@ -9,9 +9,9 @@ import {
     VerifyCustomerAccountMutationArgs,
     VerifyCustomerAccountMutationArgs,
 } from 'shared/generated-types';
 } from 'shared/generated-types';
 
 
+import { VerificationTokenError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { AuthService } from '../../service/services/auth.service';
 import { AuthService } from '../../service/services/auth.service';
 import { ChannelService } from '../../service/services/channel.service';
 import { ChannelService } from '../../service/services/channel.service';
 import { CustomerService } from '../../service/services/customer.service';
 import { CustomerService } from '../../service/services/customer.service';
@@ -99,7 +99,7 @@ export class AuthResolver {
                 res,
                 res,
             );
             );
         } else {
         } else {
-            throw new I18nError(`error.verification-token-not-recognized`);
+            throw new VerificationTokenError();
         }
         }
     }
     }
 
 

+ 2 - 2
server/src/api/resolvers/facet.resolver.ts

@@ -11,10 +11,10 @@ import {
 import { PaginatedList } from 'shared/shared-types';
 import { PaginatedList } from 'shared/shared-types';
 
 
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { Translated } from '../../common/types/locale-types';
 import { Translated } from '../../common/types/locale-types';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { Facet } from '../../entity/facet/facet.entity';
 import { Facet } from '../../entity/facet/facet.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { FacetService } from '../../service/services/facet.service';
 import { FacetService } from '../../service/services/facet.service';
 import { RequestContext } from '../common/request-context';
 import { RequestContext } from '../common/request-context';
@@ -74,7 +74,7 @@ export class FacetResolver {
         const facetId = input[0].facetId;
         const facetId = input[0].facetId;
         const facet = await this.facetService.findOne(facetId, DEFAULT_LANGUAGE_CODE);
         const facet = await this.facetService.findOne(facetId, DEFAULT_LANGUAGE_CODE);
         if (!facet) {
         if (!facet) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Facet', id: facetId });
+            throw new EntityNotFoundError('Facet', facetId);
         }
         }
         return Promise.all(input.map(facetValue => this.facetValueService.create(facet, facetValue)));
         return Promise.all(input.map(facetValue => this.facetValueService.create(facet, facetValue)));
     }
     }

+ 3 - 3
server/src/api/resolvers/order.resolver.ts

@@ -17,8 +17,8 @@ import {
 } from 'shared/generated-types';
 } from 'shared/generated-types';
 import { PaginatedList } from 'shared/shared-types';
 import { PaginatedList } from 'shared/shared-types';
 
 
+import { ForbiddenError, InternalServerError } from '../../common/error/errors';
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { OrderState } from '../../service/helpers/order-state-machine/order-state';
 import { OrderState } from '../../service/helpers/order-state-machine/order-state';
 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';
@@ -122,7 +122,7 @@ export class OrderResolver {
             }
             }
             // We throw even if the order does not exist, since giving a different response
             // We throw even if the order does not exist, since giving a different response
             // opens the door to an enumeration attack to find valid order codes.
             // opens the door to an enumeration attack to find valid order codes.
-            throw new I18nError(`error.forbidden`);
+            throw new ForbiddenError();
         }
         }
     }
     }
 
 
@@ -264,7 +264,7 @@ export class OrderResolver {
         createIfNotExists = false,
         createIfNotExists = false,
     ): Promise<Order | undefined> {
     ): Promise<Order | undefined> {
         if (!ctx.session) {
         if (!ctx.session) {
-            throw new I18nError(`error.no-active-session`);
+            throw new InternalServerError(`error.no-active-session`);
         }
         }
         let order = ctx.session.activeOrder;
         let order = ctx.session.activeOrder;
         if (!order) {
         if (!order) {

+ 2 - 5
server/src/api/resolvers/product.resolver.ts

@@ -14,11 +14,11 @@ import {
 import { ID, PaginatedList } from 'shared/shared-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 
 
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { Translated } from '../../common/types/locale-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { FacetValueService } from '../../service/services/facet-value.service';
 import { ProductVariantService } from '../../service/services/product-variant.service';
 import { ProductVariantService } from '../../service/services/product-variant.service';
 import { ProductService } from '../../service/services/product.service';
 import { ProductService } from '../../service/services/product.service';
@@ -149,10 +149,7 @@ export class ProductResolver {
             (facetValueIds as ID[]).map(async facetValueId => {
             (facetValueIds as ID[]).map(async facetValueId => {
                 const facetValue = await this.facetValueService.findOne(facetValueId, DEFAULT_LANGUAGE_CODE);
                 const facetValue = await this.facetValueService.findOne(facetValueId, DEFAULT_LANGUAGE_CODE);
                 if (!facetValue) {
                 if (!facetValue) {
-                    throw new I18nError('error.entity-with-id-not-found', {
-                        entityName: 'FacetValue',
-                        id: facetValueId,
-                    });
+                    throw new EntityNotFoundError('FacetValue', facetValueId);
                 }
                 }
                 return facetValue;
                 return facetValue;
             }),
             }),

+ 58 - 0
server/src/common/error/errors.ts

@@ -0,0 +1,58 @@
+import { ID } from 'shared/shared-types';
+
+import { coreEntitiesMap } from '../../entity/entities';
+import { I18nError } from '../../i18n/i18n-error';
+
+export class InternalServerError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message, variables, 'INTERNAL_SERVER_ERROR');
+    }
+}
+
+export class UserInputError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message, variables, 'USER_INPUT_ERROR');
+    }
+}
+
+export class IllegalOperationError extends I18nError {
+    constructor(message: string, variables: { [key: string]: string | number } = {}) {
+        super(message || 'error.cannot-transition-order-from-to', variables, 'ILLEGAL_OPERATION');
+    }
+}
+
+export class UnauthorizedError extends I18nError {
+    constructor() {
+        super('error.unauthorized', {}, 'UNAUTHORIZED');
+    }
+}
+
+export class ForbiddenError extends I18nError {
+    constructor() {
+        super('error.forbidden', {}, 'FORBIDDEN');
+    }
+}
+
+export class NoValidChannelError extends I18nError {
+    constructor() {
+        super('error.no-valid-channel-specified', {}, 'NO_VALID_CHANNEL');
+    }
+}
+
+export class EntityNotFoundError extends I18nError {
+    constructor(entityName: keyof typeof coreEntitiesMap, id: ID) {
+        super('error.entity-with-id-not-found', { entityName, id }, 'ENTITY_NOT_FOUND');
+    }
+}
+
+export class VerificationTokenError extends I18nError {
+    constructor() {
+        super('error.verification-token-not-recognized', {}, 'BAD_VERIFICATION_TOKEN');
+    }
+}
+
+export class NotVerifiedError extends I18nError {
+    constructor() {
+        super('error.email-address-not-verified', {}, 'NOT_VERIFIED');
+    }
+}

+ 2 - 2
server/src/config/asset-preview-strategy/no-asset-preview-strategy.ts

@@ -1,4 +1,4 @@
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 
 import { AssetPreviewStrategy } from './asset-preview-strategy';
 import { AssetPreviewStrategy } from './asset-preview-strategy';
 
 
@@ -7,6 +7,6 @@ import { AssetPreviewStrategy } from './asset-preview-strategy';
  */
  */
 export class NoAssetPreviewStrategy implements AssetPreviewStrategy {
 export class NoAssetPreviewStrategy implements AssetPreviewStrategy {
     generatePreviewImage(mimeType: string, data: Buffer): Promise<Buffer> {
     generatePreviewImage(mimeType: string, data: Buffer): Promise<Buffer> {
-        throw new I18nError('error.no-asset-preview-strategy-configured');
+        throw new InternalServerError('error.no-asset-preview-strategy-configured');
     }
     }
 }
 }

+ 7 - 7
server/src/config/asset-storage-strategy/no-asset-storage-strategy.ts

@@ -1,7 +1,7 @@
 import { Request } from 'express';
 import { Request } from 'express';
 import { Stream } from 'stream';
 import { Stream } from 'stream';
 
 
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 
 import { AssetStorageStrategy } from './asset-storage-strategy';
 import { AssetStorageStrategy } from './asset-storage-strategy';
 
 
@@ -12,26 +12,26 @@ const errorMessage = 'error.no-asset-storage-strategy-configured';
  */
  */
 export class NoAssetStorageStrategy implements AssetStorageStrategy {
 export class NoAssetStorageStrategy implements AssetStorageStrategy {
     writeFileFromStream(fileName: string, data: Stream): Promise<string> {
     writeFileFromStream(fileName: string, data: Stream): Promise<string> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 
 
     writeFileFromBuffer(fileName: string, data: Buffer): Promise<string> {
     writeFileFromBuffer(fileName: string, data: Buffer): Promise<string> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 
 
     readFileToBuffer(identifier: string): Promise<Buffer> {
     readFileToBuffer(identifier: string): Promise<Buffer> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 
 
     readFileToStream(identifier: string): Promise<Stream> {
     readFileToStream(identifier: string): Promise<Stream> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 
 
     toAbsoluteUrl(request: Request, identifier: string): string {
     toAbsoluteUrl(request: Request, identifier: string): string {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 
 
     fileExists(fileName: string): Promise<boolean> {
     fileExists(fileName: string): Promise<boolean> {
-        throw new I18nError(errorMessage);
+        throw new InternalServerError(errorMessage);
     }
     }
 }
 }

+ 2 - 3
server/src/entity/payment-method/payment-method.entity.ts

@@ -2,9 +2,8 @@ import { ConfigArg } from 'shared/generated-types';
 import { DeepPartial } from 'shared/shared-types';
 import { DeepPartial } from 'shared/shared-types';
 import { Column, Entity } from 'typeorm';
 import { Column, Entity } from 'typeorm';
 
 
-import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
+import { UserInputError } from '../../common/error/errors';
 import { getConfig } from '../../config/vendure-config';
 import { getConfig } from '../../config/vendure-config';
-import { I18nError } from '../../i18n/i18n-error';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
 import { Order } from '../order/order.entity';
 import { Order } from '../order/order.entity';
 import { Payment, PaymentMetadata } from '../payment/payment.entity';
 import { Payment, PaymentMetadata } from '../payment/payment.entity';
@@ -24,7 +23,7 @@ export class PaymentMethod extends VendureEntity {
     async createPayment(order: Order, metadata: PaymentMetadata) {
     async createPayment(order: Order, metadata: PaymentMetadata) {
         const handler = getConfig().paymentOptions.paymentMethodHandlers.find(h => h.code === this.code);
         const handler = getConfig().paymentOptions.paymentMethodHandlers.find(h => h.code === this.code);
         if (!handler) {
         if (!handler) {
-            throw new I18nError(`error.no-payment-handler-with-code`, { code: this.code });
+            throw new UserInputError(`error.no-payment-handler-with-code`, { code: this.code });
         }
         }
         const result = await handler.createPayment(order, this.configArgs, metadata || {});
         const result = await handler.createPayment(order, this.configArgs, metadata || {});
         return new Payment(result);
         return new Payment(result);

+ 3 - 3
server/src/entity/product-variant/product-variant.subscriber.ts

@@ -1,6 +1,6 @@
 import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
 import { EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
 
 
-import { I18nError } from '../../i18n/i18n-error';
+import { InternalServerError } from '../../common/error/errors';
 
 
 import { ProductVariantPrice } from './product-variant-price.entity';
 import { ProductVariantPrice } from './product-variant-price.entity';
 import { ProductVariant } from './product-variant.entity';
 import { ProductVariant } from './product-variant.entity';
@@ -18,7 +18,7 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ
         const { channelId, taxCategoryId } = event.queryRunner.data;
         const { channelId, taxCategoryId } = event.queryRunner.data;
         const price = event.entity.price || 0;
         const price = event.entity.price || 0;
         if (channelId === undefined) {
         if (channelId === undefined) {
-            throw new I18nError(`error.channel-id-not-set`);
+            throw new InternalServerError(`error.channel-id-not-set`);
         }
         }
         const variantPrice = new ProductVariantPrice({ price, channelId });
         const variantPrice = new ProductVariantPrice({ price, channelId });
         variantPrice.variant = event.entity;
         variantPrice.variant = event.entity;
@@ -33,7 +33,7 @@ export class ProductVariantSubscriber implements EntitySubscriberInterface<Produ
             },
             },
         });
         });
         if (!variantPrice) {
         if (!variantPrice) {
-            throw new I18nError(`error.could-not-find-product-variant-price`);
+            throw new InternalServerError(`error.could-not-find-product-variant-price`);
         }
         }
 
 
         variantPrice.price = event.entity.price || 0;
         variantPrice.price = event.entity.price || 0;

+ 10 - 4
server/src/i18n/i18n-error.ts

@@ -1,5 +1,7 @@
+import { ApolloError } from 'apollo-server-core';
+
 /**
 /**
- * All errors thrown in the Vendure server must use this error class. This allows the
+ * All errors thrown in the Vendure server must use or extend this error class. This allows the
  * error message to be translated before being served to the client.
  * error message to be translated before being served to the client.
  *
  *
  * The message should be of the form `Could not find user {{ id }}`, with the variables argument
  * The message should be of the form `Could not find user {{ id }}`, with the variables argument
@@ -10,8 +12,12 @@
  * throw new I18nError(`Could not find user {{ id }}`, { id });
  * throw new I18nError(`Could not find user {{ id }}`, { id });
  * ```
  * ```
  */
  */
-export class I18nError extends Error {
-    constructor(public message: string, public variables: { [key: string]: string | number } = {}) {
-        super(message);
+export abstract class I18nError extends ApolloError {
+    protected constructor(
+        public message: string,
+        public variables: { [key: string]: string | number } = {},
+        code?: string,
+    ) {
+        super(message, code);
     }
     }
 }
 }

+ 3 - 0
server/src/i18n/i18n.service.ts

@@ -63,6 +63,9 @@ export class I18nService {
                 translation += ` (Translation format error: ${e.message})`;
                 translation += ` (Translation format error: ${e.message})`;
             }
             }
             error.message = translation;
             error.message = translation;
+            // We can now safely remove the variables object so that they do not appear in
+            // the error returned by the GraphQL API
+            delete originalError.variables;
         }
         }
 
 
         return error;
         return error;

+ 3 - 2
server/src/i18n/messages/en.json

@@ -8,13 +8,14 @@
     "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",
-    "forbidden": "This action is forbidden",
+    "forbidden": "You are not currently authorized to perform this action",
     "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 }",
     "no-valid-channel-specified": "No valid channel was specified",
     "no-valid-channel-specified": "No valid channel was specified",
     "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
     "order-contents-may-only-be-modified-in-addingitems-state": "Order contents may only be modified when in the \"AddingItems\" state",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
     "order-does-not-contain-line-with-id": "This order does not contain an OrderLine with the id { id }",
     "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem",
     "order-item-quantity-must-be-positive": "{ quantity } is not a valid quantity for an OrderItem",
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
-    "verification-token-not-recognized": "Verification token not recognized"
+    "verification-token-not-recognized": "Verification token not recognized",
+    "unauthorized": "The credentials did not match. Please check and try again"
   }
   }
 }
 }

+ 2 - 2
server/src/service/helpers/list-query-builder/parse-filter-params.ts

@@ -4,8 +4,8 @@ import { Connection } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
+import { UserInputError } from '../../../common/error/errors';
 import {
 import {
     BooleanOperators,
     BooleanOperators,
     DateOperators,
     DateOperators,
@@ -55,7 +55,7 @@ export function parseFilterParams<T extends VendureEntity>(
                 } else if (translationColumns.find(c => c.propertyName === key)) {
                 } else if (translationColumns.find(c => c.propertyName === key)) {
                     fieldName = `${alias}_translations.${key}`;
                     fieldName = `${alias}_translations.${key}`;
                 } else {
                 } else {
-                    throw new I18nError('error.invalid-filter-field');
+                    throw new UserInputError('error.invalid-filter-field');
                 }
                 }
                 const condition = buildWhereCondition(fieldName, operator as Operator, operand);
                 const condition = buildWhereCondition(fieldName, operator as Operator, operand);
                 output.push(condition);
                 output.push(condition);

+ 2 - 2
server/src/service/helpers/list-query-builder/parse-sort-params.ts

@@ -3,8 +3,8 @@ import { Connection, OrderByCondition } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
+import { UserInputError } from '../../../common/error/errors';
 import { NullOptionals, SortParameter } from '../../../common/types/common-types';
 import { NullOptionals, SortParameter } from '../../../common/types/common-types';
 
 
 /**
 /**
@@ -43,7 +43,7 @@ export function parseSortParams<T extends VendureEntity>(
         } else if (translationColumns.find(c => c.propertyName === key)) {
         } else if (translationColumns.find(c => c.propertyName === key)) {
             output[`${alias}_translations.${key}`] = order;
             output[`${alias}_translations.${key}`] = order;
         } else {
         } else {
-            throw new I18nError('error.invalid-sort-field', {
+            throw new UserInputError('error.invalid-sort-field', {
                 fieldName: key,
                 fieldName: key,
                 validFields: getValidSortFields([...columns, ...translationColumns]),
                 validFields: getValidSortFields([...columns, ...translationColumns]),
             });
             });

+ 5 - 6
server/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -1,12 +1,12 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
+import { IllegalOperationError } from '../../../common/error/errors';
 import { FSM, StateMachineConfig, Transitions } from '../../../common/finite-state-machine';
 import { FSM, StateMachineConfig, Transitions } from '../../../common/finite-state-machine';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { EventBus } from '../../../event-bus/event-bus';
 import { EventBus } from '../../../event-bus/event-bus';
 import { OrderStateTransitionEvent } from '../../../event-bus/events/order-state-transition-event';
 import { OrderStateTransitionEvent } from '../../../event-bus/events/order-state-transition-event';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
 import { OrderState, orderStateTransitions, OrderTransitionData } from './order-state';
 import { OrderState, orderStateTransitions, OrderTransitionData } from './order-state';
 
 
@@ -91,11 +91,10 @@ export class OrderStateMachine {
                 if (typeof onError === 'function') {
                 if (typeof onError === 'function') {
                     onError(fromState, toState, message);
                     onError(fromState, toState, message);
                 }
                 }
-                if (!message) {
-                    throw new I18nError(`error.cannot-transition-order-from-to`, { fromState, toState });
-                } else {
-                    throw new I18nError(message);
-                }
+                throw new IllegalOperationError(message || 'error.cannot-transition-order-from-to', {
+                    fromState,
+                    toState,
+                });
             },
             },
         };
         };
     }
     }

+ 2 - 2
server/src/service/helpers/translatable-saver/translation-differ.ts

@@ -1,9 +1,9 @@
 import { DeepPartial } from 'shared/shared-types';
 import { DeepPartial } from 'shared/shared-types';
 import { EntityManager } from 'typeorm';
 import { EntityManager } from 'typeorm';
 
 
+import { EntityNotFoundError } from '../../../common/error/errors';
 import { Translatable, Translation, TranslationInput } from '../../../common/types/locale-types';
 import { Translatable, Translation, TranslationInput } from '../../../common/types/locale-types';
 import { foundIn, not } from '../../../common/utils';
 import { foundIn, not } from '../../../common/utils';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
 export interface TranslationContructor<T> {
 export interface TranslationContructor<T> {
     new (input?: DeepPartial<TranslationInput<T>> | DeepPartial<Translation<T>>): Translation<T>;
     new (input?: DeepPartial<TranslationInput<T>> | DeepPartial<Translation<T>>): Translation<T>;
@@ -70,7 +70,7 @@ export class TranslationDiffer<Entity extends Translatable> {
                 } catch (err) {
                 } catch (err) {
                     const entityName = entity.constructor.name;
                     const entityName = entity.constructor.name;
                     const id = (entity as any).id || 'undefined';
                     const id = (entity as any).id || 'undefined';
-                    throw new I18nError('error.entity-with-id-not-found', { entityName, id });
+                    throw new EntityNotFoundError(entityName as any, id);
                 }
                 }
                 entity.translations.push(newTranslation);
                 entity.translations.push(newTranslation);
             }
             }

+ 2 - 5
server/src/service/helpers/utils/get-entity-or-throw.ts

@@ -1,8 +1,8 @@
 import { ID, Type } from 'shared/shared-types';
 import { ID, Type } from 'shared/shared-types';
 import { Connection, FindOneOptions } from 'typeorm';
 import { Connection, FindOneOptions } from 'typeorm';
 
 
+import { EntityNotFoundError } from '../../../common/error/errors';
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
 /**
 /**
  * Attempts to find an entity of the given type and id, and throws an error if not found.
  * Attempts to find an entity of the given type and id, and throws an error if not found.
@@ -15,10 +15,7 @@ export async function getEntityOrThrow<T extends VendureEntity>(
 ): Promise<T> {
 ): Promise<T> {
     const entity = await connection.getRepository(entityType).findOne(id, findOptions);
     const entity = await connection.getRepository(entityType).findOne(id, findOptions);
     if (!entity) {
     if (!entity) {
-        throw new I18nError(`error.entity-with-id-not-found`, {
-            entityName: entityType.name,
-            id,
-        });
+        throw new EntityNotFoundError(entityType.name as any, id);
     }
     }
     return entity;
     return entity;
 }
 }

+ 2 - 2
server/src/service/helpers/utils/translate-entity.ts

@@ -1,8 +1,8 @@
 import { LanguageCode } from 'shared/generated-types';
 import { LanguageCode } from 'shared/generated-types';
 
 
 import { UnwrappedArray } from '../../../common/types/common-types';
 import { UnwrappedArray } from '../../../common/types/common-types';
-import { I18nError } from '../../../i18n/i18n-error';
 
 
+import { InternalServerError } from '../../../common/error/errors';
 import { Translatable, Translated } from '../../../common/types/locale-types';
 import { Translatable, Translated } from '../../../common/types/locale-types';
 
 
 // prettier-ignore
 // prettier-ignore
@@ -42,7 +42,7 @@ export function translateEntity<T extends Translatable>(
         translatable.translations && translatable.translations.find(t => t.languageCode === languageCode);
         translatable.translations && translatable.translations.find(t => t.languageCode === languageCode);
 
 
     if (!translation) {
     if (!translation) {
-        throw new I18nError(`error.entity-has-no-translation-in-language`, {
+        throw new InternalServerError(`error.entity-has-no-translation-in-language`, {
             entityName: translatable.constructor.name,
             entityName: translatable.constructor.name,
             languageCode,
             languageCode,
         });
         });

+ 4 - 10
server/src/service/services/administrator.service.ts

@@ -5,10 +5,10 @@ import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from 'shared/s
 import { ID, PaginatedList } from 'shared/shared-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -59,10 +59,7 @@ export class AdministratorService {
     async update(input: UpdateAdministratorInput): Promise<Administrator> {
     async update(input: UpdateAdministratorInput): Promise<Administrator> {
         const administrator = await this.findOne(input.id);
         const administrator = await this.findOne(input.id);
         if (!administrator) {
         if (!administrator) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'Administrator',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Administrator', input.id);
         }
         }
         let updatedAdministrator = patchEntity(administrator, input);
         let updatedAdministrator = patchEntity(administrator, input);
         await this.connection.manager.save(administrator);
         await this.connection.manager.save(administrator);
@@ -86,14 +83,11 @@ export class AdministratorService {
     async assignRole(administratorId: ID, roleId: ID): Promise<Administrator> {
     async assignRole(administratorId: ID, roleId: ID): Promise<Administrator> {
         const administrator = await this.findOne(administratorId);
         const administrator = await this.findOne(administratorId);
         if (!administrator) {
         if (!administrator) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                id: administratorId,
-                entityName: 'Administrator',
-            });
+            throw new EntityNotFoundError('Administrator', administratorId);
         }
         }
         const role = await this.roleService.findOne(roleId);
         const role = await this.roleService.findOne(roleId);
         if (!role) {
         if (!role) {
-            throw new I18nError(`error.entity-with-id-not-found`, { id: roleId, entityName: 'Role' });
+            throw new EntityNotFoundError('Role', roleId);
         }
         }
         administrator.user.roles.push(role);
         administrator.user.roles.push(role);
         await this.connection.manager.save(administrator.user);
         await this.connection.manager.save(administrator.user);

+ 5 - 5
server/src/service/services/auth.service.ts

@@ -1,17 +1,17 @@
-import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { InjectConnection } from '@nestjs/typeorm';
 import * as crypto from 'crypto';
 import * as crypto from 'crypto';
 import * as ms from 'ms';
 import * as ms from 'ms';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { NotVerifiedError, UnauthorizedError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
 import { AnonymousSession } from '../../entity/session/anonymous-session.entity';
 import { AnonymousSession } from '../../entity/session/anonymous-session.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
 
 
 import { OrderService } from './order.service';
 import { OrderService } from './order.service';
@@ -43,10 +43,10 @@ export class AuthService {
         const user = await this.getUserFromIdentifier(identifier);
         const user = await this.getUserFromIdentifier(identifier);
         const passwordMatches = await this.passwordCipher.check(password, user.passwordHash);
         const passwordMatches = await this.passwordCipher.check(password, user.passwordHash);
         if (!passwordMatches) {
         if (!passwordMatches) {
-            throw new UnauthorizedException();
+            throw new UnauthorizedError();
         }
         }
         if (this.configService.authOptions.requireVerification && !user.verified) {
         if (this.configService.authOptions.requireVerification && !user.verified) {
-            throw new I18nError(`error.email-address-not-verified`);
+            throw new NotVerifiedError();
         }
         }
         await this.deleteSessionsByUser(user);
         await this.deleteSessionsByUser(user);
         if (ctx.session && ctx.session.activeOrder) {
         if (ctx.session && ctx.session.activeOrder) {
@@ -153,7 +153,7 @@ export class AuthService {
             relations: ['roles', 'roles.channels'],
             relations: ['roles', 'roles.channels'],
         });
         });
         if (!user) {
         if (!user) {
-            throw new UnauthorizedException();
+            throw new UnauthorizedError();
         }
         }
         return user;
         return user;
     }
     }

+ 4 - 7
server/src/service/services/channel.service.ts

@@ -7,12 +7,12 @@ import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ChannelAware } from '../../common/types/common-types';
 import { ChannelAware } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { Zone } from '../../entity/zone/zone.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
@@ -47,7 +47,7 @@ export class ChannelService {
     getChannelFromToken(token: string): Channel {
     getChannelFromToken(token: string): Channel {
         const channel = this.allChannels.find(c => c.token === token);
         const channel = this.allChannels.find(c => c.token === token);
         if (!channel) {
         if (!channel) {
-            throw new I18nError(`error.channel-not-found`, { token });
+            throw new InternalServerError(`error.channel-not-found`, { token });
         }
         }
         return channel;
         return channel;
     }
     }
@@ -59,7 +59,7 @@ export class ChannelService {
         const defaultChannel = this.allChannels.find(channel => channel.code === DEFAULT_CHANNEL_CODE);
         const defaultChannel = this.allChannels.find(channel => channel.code === DEFAULT_CHANNEL_CODE);
 
 
         if (!defaultChannel) {
         if (!defaultChannel) {
-            throw new I18nError(`error.default-channel-not-found`);
+            throw new InternalServerError(`error.default-channel-not-found`);
         }
         }
         return defaultChannel;
         return defaultChannel;
     }
     }
@@ -96,10 +96,7 @@ export class ChannelService {
     async update(input: UpdateChannelInput): Promise<Channel> {
     async update(input: UpdateChannelInput): Promise<Channel> {
         const channel = await this.findOne(input.id);
         const channel = await this.findOne(input.id);
         if (!channel) {
         if (!channel) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'Channel',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Channel', input.id);
         }
         }
         const updatedChannel = patchEntity(channel, input);
         const updatedChannel = patchEntity(channel, input);
         if (input.defaultTaxZoneId) {
         if (input.defaultTaxZoneId) {

+ 3 - 3
server/src/service/services/customer.service.ts

@@ -11,13 +11,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound, normalizeEmailAddress } from '../../common/utils';
 import { assertFound, normalizeEmailAddress } from '../../common/utils';
 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 { EventBus } from '../../event-bus/event-bus';
 import { EventBus } from '../../event-bus/event-bus';
 import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
 import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -71,7 +71,7 @@ export class CustomerService {
         });
         });
 
 
         if (existing) {
         if (existing) {
-            throw new I18nError(`error.email-address-must-be-unique`);
+            throw new InternalServerError(`error.email-address-must-be-unique`);
         }
         }
 
 
         if (password) {
         if (password) {
@@ -155,7 +155,7 @@ export class CustomerService {
         });
         });
 
 
         if (!customer) {
         if (!customer) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Customer', id: customerId });
+            throw new EntityNotFoundError('Customer', customerId);
         }
         }
 
 
         const address = new Address(input);
         const address = new Address(input);

+ 9 - 12
server/src/service/services/order.service.ts

@@ -4,6 +4,7 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, IllegalOperationError, UserInputError } from '../../common/error/errors';
 import { generatePublicId } from '../../common/generate-public-id';
 import { generatePublicId } from '../../common/generate-public-id';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { idsAreEqual } from '../../common/utils';
 import { idsAreEqual } from '../../common/utils';
@@ -15,7 +16,6 @@ import { Payment } from '../../entity/payment/payment.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
 import { OrderMerger } from '../helpers/order-merger/order-merger';
@@ -219,7 +219,7 @@ export class OrderService {
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
         const selectedMethod = eligibleMethods.find(m => idsAreEqual(m.method.id, shippingMethodId));
         const selectedMethod = eligibleMethods.find(m => idsAreEqual(m.method.id, shippingMethodId));
         if (!selectedMethod) {
         if (!selectedMethod) {
-            throw new I18nError(`error.shipping-method-unavailable`);
+            throw new UserInputError(`error.shipping-method-unavailable`);
         }
         }
         order.shippingMethod = selectedMethod.method;
         order.shippingMethod = selectedMethod.method;
         await this.connection.getRepository(Order).save(order);
         await this.connection.getRepository(Order).save(order);
@@ -237,7 +237,7 @@ export class OrderService {
     async addPaymentToOrder(ctx: RequestContext, orderId: ID, input: PaymentInput): Promise<Order> {
     async addPaymentToOrder(ctx: RequestContext, orderId: ID, input: PaymentInput): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.state !== 'ArrangingPayment') {
         if (order.state !== 'ArrangingPayment') {
-            throw new I18nError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
+            throw new IllegalOperationError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
         }
         }
         const payment = await this.paymentMethodService.createPayment(order, input.method, input.metadata);
         const payment = await this.paymentMethodService.createPayment(order, input.method, input.metadata);
         if (order.payments) {
         if (order.payments) {
@@ -259,7 +259,7 @@ export class OrderService {
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
     async addCustomerToOrder(ctx: RequestContext, orderId: ID, customer: Customer): Promise<Order> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const order = await this.getOrderOrThrow(ctx, orderId);
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {
         if (order.customer && !idsAreEqual(order.customer.id, customer.id)) {
-            throw new I18nError(`error.order-already-has-customer`);
+            throw new IllegalOperationError(`error.order-already-has-customer`);
         }
         }
         order.customer = customer;
         order.customer = customer;
         return this.connection.getRepository(Order).save(order);
         return this.connection.getRepository(Order).save(order);
@@ -297,7 +297,7 @@ export class OrderService {
     private async getOrderOrThrow(ctx: RequestContext, orderId: ID): Promise<Order> {
     private async getOrderOrThrow(ctx: RequestContext, orderId: ID): Promise<Order> {
         const order = await this.findOne(ctx, orderId);
         const order = await this.findOne(ctx, orderId);
         if (!order) {
         if (!order) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Order', id: orderId });
+            throw new EntityNotFoundError('Order', orderId);
         }
         }
         return order;
         return order;
     }
     }
@@ -308,10 +308,7 @@ export class OrderService {
     ): Promise<ProductVariant> {
     ): Promise<ProductVariant> {
         const productVariant = await this.productVariantService.findOne(ctx, productVariantId);
         const productVariant = await this.productVariantService.findOne(ctx, productVariantId);
         if (!productVariant) {
         if (!productVariant) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'ProductVariant',
-                id: productVariantId,
-            });
+            throw new EntityNotFoundError('ProductVariant', productVariantId);
         }
         }
         return productVariant;
         return productVariant;
     }
     }
@@ -319,7 +316,7 @@ export class OrderService {
     private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
     private getOrderLineOrThrow(order: Order, orderLineId: ID): OrderLine {
         const orderItem = order.lines.find(line => idsAreEqual(line.id, orderLineId));
         const orderItem = order.lines.find(line => idsAreEqual(line.id, orderLineId));
         if (!orderItem) {
         if (!orderItem) {
-            throw new I18nError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
+            throw new UserInputError(`error.order-does-not-contain-line-with-id`, { id: orderLineId });
         }
         }
         return orderItem;
         return orderItem;
     }
     }
@@ -337,7 +334,7 @@ export class OrderService {
      */
      */
     private assertQuantityIsPositive(quantity: number) {
     private assertQuantityIsPositive(quantity: number) {
         if (quantity < 0) {
         if (quantity < 0) {
-            throw new I18nError(`error.order-item-quantity-must-be-positive`, { quantity });
+            throw new IllegalOperationError(`error.order-item-quantity-must-be-positive`, { quantity });
         }
         }
     }
     }
 
 
@@ -346,7 +343,7 @@ export class OrderService {
      */
      */
     private assertAddingItemsState(order: Order) {
     private assertAddingItemsState(order: Order) {
         if (order.state !== 'AddingItems') {
         if (order.state !== 'AddingItems') {
-            throw new I18nError(`error.order-contents-may-only-be-modified-in-addingitems-state`);
+            throw new IllegalOperationError(`error.order-contents-may-only-be-modified-in-addingitems-state`);
         }
         }
     }
     }
 
 

+ 2 - 2
server/src/service/services/payment-method.service.ts

@@ -6,6 +6,7 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { assertNever } from 'shared/shared-utils';
 import { assertNever } from 'shared/shared-utils';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { UserInputError } from '../../common/error/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 {
 import {
@@ -16,7 +17,6 @@ import {
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { PaymentMethod } from '../../entity/payment-method/payment-method.entity';
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -73,7 +73,7 @@ export class PaymentMethodService {
             },
             },
         });
         });
         if (!paymentMethod) {
         if (!paymentMethod) {
-            throw new I18nError(`error.payment-method-not-found`, { method });
+            throw new UserInputError(`error.payment-method-not-found`, { method });
         }
         }
         const payment = await paymentMethod.createPayment(order, metadata);
         const payment = await paymentMethod.createPayment(order, metadata);
         return this.connection.getRepository(Payment).save(payment);
         return this.connection.getRepository(Payment).save(payment);

+ 4 - 7
server/src/service/services/product-variant.service.ts

@@ -7,6 +7,7 @@ import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 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';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
@@ -14,7 +15,6 @@ import { ProductOption } from '../../entity/product-option/product-option.entity
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -128,7 +128,7 @@ export class ProductVariantService {
         });
         });
 
 
         if (!product) {
         if (!product) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Product', id: productId });
+            throw new EntityNotFoundError('Product', productId);
         }
         }
         const defaultTranslation = product.translations.find(t => t.languageCode === DEFAULT_LANGUAGE_CODE);
         const defaultTranslation = product.translations.find(t => t.languageCode === DEFAULT_LANGUAGE_CODE);
 
 
@@ -172,10 +172,7 @@ export class ProductVariantService {
 
 
         const notFoundIds = productVariantIds.filter(id => !variants.find(v => idsAreEqual(v.id, id)));
         const notFoundIds = productVariantIds.filter(id => !variants.find(v => idsAreEqual(v.id, id)));
         if (notFoundIds.length) {
         if (notFoundIds.length) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'ProductVariant',
-                id: notFoundIds[0],
-            });
+            throw new EntityNotFoundError('ProductVariant', notFoundIds[0]);
         }
         }
         for (const variant of variants) {
         for (const variant of variants) {
             for (const facetValue of facetValues) {
             for (const facetValue of facetValues) {
@@ -200,7 +197,7 @@ export class ProductVariantService {
     applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): ProductVariant {
     applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): ProductVariant {
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         if (!channelPrice) {
         if (!channelPrice) {
-            throw new I18nError(`error.no-price-found-for-channel`);
+            throw new InternalServerError(`error.no-price-found-for-channel`);
         }
         }
         const applicableTaxRate = this.taxRateService.getApplicableTaxRate(
         const applicableTaxRate = this.taxRateService.getApplicableTaxRate(
             ctx.activeTaxZone,
             ctx.activeTaxZone,

+ 3 - 6
server/src/service/services/product.service.ts

@@ -5,13 +5,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError } from '../../common/error/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 } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductTranslation } from '../../entity/product/product-translation.entity';
 import { ProductTranslation } from '../../entity/product/product-translation.entity';
 import { Product } from '../../entity/product/product.entity';
 import { Product } from '../../entity/product/product.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -93,10 +93,7 @@ export class ProductService {
         const product = await this.getProductWithOptionGroups(productId);
         const product = await this.getProductWithOptionGroups(productId);
         const optionGroup = await this.connection.getRepository(ProductOptionGroup).findOne(optionGroupId);
         const optionGroup = await this.connection.getRepository(ProductOptionGroup).findOne(optionGroupId);
         if (!optionGroup) {
         if (!optionGroup) {
-            throw new I18nError('error.entity-with-id-not-found', {
-                entityName: 'OptionGroup',
-                id: optionGroupId,
-            });
+            throw new EntityNotFoundError('ProductOptionGroup', optionGroupId);
         }
         }
 
 
         if (Array.isArray(product.optionGroups)) {
         if (Array.isArray(product.optionGroups)) {
@@ -142,7 +139,7 @@ export class ProductService {
             .getRepository(Product)
             .getRepository(Product)
             .findOne(productId, { relations: ['optionGroups'] });
             .findOne(productId, { relations: ['optionGroups'] });
         if (!product) {
         if (!product) {
-            throw new I18nError('error.entity-with-id-not-found', { entityName: 'Product', id: productId });
+            throw new EntityNotFoundError('Product', productId);
         }
         }
         return product;
         return product;
     }
     }

+ 3 - 6
server/src/service/services/promotion.service.ts

@@ -11,13 +11,13 @@ import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
+import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { PromotionAction } from '../../config/promotion/promotion-action';
 import { PromotionAction } 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';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
@@ -105,10 +105,7 @@ export class PromotionService {
     async updatePromotion(ctx: RequestContext, input: UpdatePromotionInput): Promise<Promotion> {
     async updatePromotion(ctx: RequestContext, input: UpdatePromotionInput): Promise<Promotion> {
         const adjustmentSource = await this.connection.getRepository(Promotion).findOne(input.id);
         const adjustmentSource = await this.connection.getRepository(Promotion).findOne(input.id);
         if (!adjustmentSource) {
         if (!adjustmentSource) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'AdjustmentSource',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('Promotion', input.id);
         }
         }
         const updatedAdjustmentSource = patchEntity(adjustmentSource, omit(input, ['conditions', 'actions']));
         const updatedAdjustmentSource = patchEntity(adjustmentSource, omit(input, ['conditions', 'actions']));
         if (input.conditions) {
         if (input.conditions) {
@@ -165,7 +162,7 @@ export class PromotionService {
             type === 'condition' ? this.availableConditions : this.availableActions;
             type === 'condition' ? this.availableConditions : this.availableActions;
         const match = available.find(a => a.code === code);
         const match = available.find(a => a.code === code);
         if (!match) {
         if (!match) {
-            throw new I18nError(`error.adjustment-operation-with-code-not-found`, { code });
+            throw new UserInputError(`error.adjustment-operation-with-code-not-found`, { code });
         }
         }
         return match;
         return match;
     }
     }

+ 5 - 5
server/src/service/services/role.service.ts

@@ -10,10 +10,10 @@ import {
 import { ID, PaginatedList } from 'shared/shared-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { Role } from '../../entity/role/role.entity';
 import { Role } from '../../entity/role/role.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
@@ -51,7 +51,7 @@ export class RoleService {
     getSuperAdminRole(): Promise<Role> {
     getSuperAdminRole(): Promise<Role> {
         return this.getRoleByCode(SUPER_ADMIN_ROLE_CODE).then(role => {
         return this.getRoleByCode(SUPER_ADMIN_ROLE_CODE).then(role => {
             if (!role) {
             if (!role) {
-                throw new I18nError(`error.super-admin-role-not-found`);
+                throw new InternalServerError(`error.super-admin-role-not-found`);
             }
             }
             return role;
             return role;
         });
         });
@@ -60,7 +60,7 @@ export class RoleService {
     getCustomerRole(): Promise<Role> {
     getCustomerRole(): Promise<Role> {
         return this.getRoleByCode(CUSTOMER_ROLE_CODE).then(role => {
         return this.getRoleByCode(CUSTOMER_ROLE_CODE).then(role => {
             if (!role) {
             if (!role) {
-                throw new I18nError(`error.customer-role-not-found`);
+                throw new InternalServerError(`error.customer-role-not-found`);
             }
             }
             return role;
             return role;
         });
         });
@@ -75,10 +75,10 @@ export class RoleService {
     async update(input: UpdateRoleInput): Promise<Role> {
     async update(input: UpdateRoleInput): Promise<Role> {
         const role = await this.findOne(input.id);
         const role = await this.findOne(input.id);
         if (!role) {
         if (!role) {
-            throw new I18nError(`error.entity-with-id-not-found`, { entityName: 'Role', id: input.id });
+            throw new EntityNotFoundError('Role', input.id);
         }
         }
         if (role.code === SUPER_ADMIN_ROLE_CODE || role.code === CUSTOMER_ROLE_CODE) {
         if (role.code === SUPER_ADMIN_ROLE_CODE || role.code === CUSTOMER_ROLE_CODE) {
-            throw new I18nError(`error.cannot-modify-role`, { roleCode: role.code });
+            throw new InternalServerError(`error.cannot-modify-role`, { roleCode: role.code });
         }
         }
         const updatedRole = patchEntity(role, input);
         const updatedRole = patchEntity(role, input);
         await this.connection.manager.save(updatedRole);
         await this.connection.manager.save(updatedRole);

+ 4 - 7
server/src/service/services/shipping-method.service.ts

@@ -10,6 +10,7 @@ import { omit } from 'shared/omit';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
@@ -17,7 +18,6 @@ import { ShippingCalculator } from '../../config/shipping-method/shipping-calcul
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
@@ -76,10 +76,7 @@ export class ShippingMethodService {
     async update(input: UpdateShippingMethodInput): Promise<ShippingMethod> {
     async update(input: UpdateShippingMethodInput): Promise<ShippingMethod> {
         const shippingMethod = await this.findOne(input.id);
         const shippingMethod = await this.findOne(input.id);
         if (!shippingMethod) {
         if (!shippingMethod) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'ShippingMethod',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('ShippingMethod', input.id);
         }
         }
         const updatedShippingMethod = patchEntity(shippingMethod, omit(input, ['checker', 'calculator']));
         const updatedShippingMethod = patchEntity(shippingMethod, omit(input, ['checker', 'calculator']));
         if (input.checker) {
         if (input.checker) {
@@ -143,7 +140,7 @@ export class ShippingMethodService {
     private getChecker(code: string): ShippingEligibilityChecker {
     private getChecker(code: string): ShippingEligibilityChecker {
         const match = this.shippingEligibilityCheckers.find(a => a.code === code);
         const match = this.shippingEligibilityCheckers.find(a => a.code === code);
         if (!match) {
         if (!match) {
-            throw new I18nError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
+            throw new UserInputError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
         }
         }
         return match;
         return match;
     }
     }
@@ -151,7 +148,7 @@ export class ShippingMethodService {
     private getCalculator(code: string): ShippingCalculator {
     private getCalculator(code: string): ShippingCalculator {
         const match = this.shippingCalculators.find(a => a.code === code);
         const match = this.shippingCalculators.find(a => a.code === code);
         if (!match) {
         if (!match) {
-            throw new I18nError(`error.shipping-calculator-with-code-not-found`, { code });
+            throw new UserInputError(`error.shipping-calculator-with-code-not-found`, { code });
         }
         }
         return match;
         return match;
     }
     }

+ 2 - 5
server/src/service/services/tax-category.service.ts

@@ -4,9 +4,9 @@ import { CreateTaxCategoryInput, UpdateTaxCategoryInput } from 'shared/generated
 import { ID } from 'shared/shared-types';
 import { ID } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
 @Injectable()
 @Injectable()
@@ -30,10 +30,7 @@ export class TaxCategoryService {
     async update(input: UpdateTaxCategoryInput): Promise<TaxCategory> {
     async update(input: UpdateTaxCategoryInput): Promise<TaxCategory> {
         const taxCategory = await this.findOne(input.id);
         const taxCategory = await this.findOne(input.id);
         if (!taxCategory) {
         if (!taxCategory) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'TaxCategory',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('TaxCategory', input.id);
         }
         }
         const updatedTaxCategory = patchEntity(taxCategory, input);
         const updatedTaxCategory = patchEntity(taxCategory, input);
         await this.connection.getRepository(TaxCategory).save(updatedTaxCategory);
         await this.connection.getRepository(TaxCategory).save(updatedTaxCategory);

+ 2 - 5
server/src/service/services/tax-rate.service.ts

@@ -3,13 +3,13 @@ import { CreateTaxRateInput, UpdateTaxRateInput } from 'shared/generated-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { ID, PaginatedList } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { assertFound } from '../../common/utils';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { Zone } from '../../entity/zone/zone.entity';
-import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
@@ -71,10 +71,7 @@ export class TaxRateService {
     async update(input: UpdateTaxRateInput): Promise<TaxRate> {
     async update(input: UpdateTaxRateInput): Promise<TaxRate> {
         const taxRate = await this.findOne(input.id);
         const taxRate = await this.findOne(input.id);
         if (!taxRate) {
         if (!taxRate) {
-            throw new I18nError(`error.entity-with-id-not-found`, {
-                entityName: 'TaxRate',
-                id: input.id,
-            });
+            throw new EntityNotFoundError('TaxRate', input.id);
         }
         }
         const updatedTaxRate = patchEntity(taxRate, input);
         const updatedTaxRate = patchEntity(taxRate, input);
         if (input.categoryId) {
         if (input.categoryId) {

+ 2 - 5
server/yarn.lock

@@ -239,15 +239,12 @@
 "@types/i18next-express-middleware@^0.0.33":
 "@types/i18next-express-middleware@^0.0.33":
   version "0.0.33"
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/@types/i18next-express-middleware/-/i18next-express-middleware-0.0.33.tgz#1c5625f123eaae126de3b43626ef9a04bc6ad482"
   resolved "https://registry.yarnpkg.com/@types/i18next-express-middleware/-/i18next-express-middleware-0.0.33.tgz#1c5625f123eaae126de3b43626ef9a04bc6ad482"
+  integrity sha512-lXuok/3HcT81FipU00+5W1cRjSpl5y0ImQKV2wxCaRkLfANMBi+T7idOVdhZutsX0OlnR2YxLM1I/NuTvYetoQ==
   dependencies:
   dependencies:
     "@types/express" "*"
     "@types/express" "*"
     "@types/i18next" "*"
     "@types/i18next" "*"
 
 
-"@types/i18next@*":
-  version "8.4.4"
-  resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-8.4.4.tgz#76591ab5974b65910e44b8860202d4f544f39b9d"
-
-"@types/i18next@^11.9.3":
+"@types/i18next@*", "@types/i18next@^11.9.3":
   version "11.9.3"
   version "11.9.3"
   resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-11.9.3.tgz#04d84c6539908ad69665d26d8967f942d1638550"
   resolved "https://registry.yarnpkg.com/@types/i18next/-/i18next-11.9.3.tgz#04d84c6539908ad69665d26d8967f942d1638550"
   integrity sha512-snM7bMKy6gt7UYdpjsxycqSCAy0fr2JVPY0B8tJ2vp9bN58cE7C880k20PWFM4KXxQ3KsstKM8DLCawGCIH0tg==
   integrity sha512-snM7bMKy6gt7UYdpjsxycqSCAy0fr2JVPY0B8tJ2vp9bN58cE7C880k20PWFM4KXxQ3KsstKM8DLCawGCIH0tg==

+ 3 - 0
shared/generated-types.ts

@@ -85,6 +85,7 @@ export interface Query {
     taxRate?: TaxRate | null;
     taxRate?: TaxRate | null;
     zones: Zone[];
     zones: Zone[];
     zone?: Zone | null;
     zone?: Zone | null;
+    temp__?: boolean | null;
     networkStatus: NetworkStatus;
     networkStatus: NetworkStatus;
     userStatus: UserStatus;
     userStatus: UserStatus;
     uiState: UiState;
     uiState: UiState;
@@ -1883,6 +1884,7 @@ export namespace QueryResolvers {
         taxRate?: TaxRateResolver<TaxRate | null, any, Context>;
         taxRate?: TaxRateResolver<TaxRate | null, any, Context>;
         zones?: ZonesResolver<Zone[], any, Context>;
         zones?: ZonesResolver<Zone[], any, Context>;
         zone?: ZoneResolver<Zone | null, any, Context>;
         zone?: ZoneResolver<Zone | null, any, Context>;
+        temp__?: TempResolver<boolean | null, any, Context>;
         networkStatus?: NetworkStatusResolver<NetworkStatus, any, Context>;
         networkStatus?: NetworkStatusResolver<NetworkStatus, any, Context>;
         userStatus?: UserStatusResolver<UserStatus, any, Context>;
         userStatus?: UserStatusResolver<UserStatus, any, Context>;
         uiState?: UiStateResolver<UiState, any, Context>;
         uiState?: UiStateResolver<UiState, any, Context>;
@@ -2266,6 +2268,7 @@ export namespace QueryResolvers {
         id: string;
         id: string;
     }
     }
 
 
+    export type TempResolver<R = boolean | null, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type NetworkStatusResolver<R = NetworkStatus, Parent = any, Context = any> = Resolver<
     export type NetworkStatusResolver<R = NetworkStatus, Parent = any, Context = any> = Resolver<
         R,
         R,
         Parent,
         Parent,

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini