Преглед изворни кода

feat(core): Make Promotion entity translatable, add description

Relates to #1990

BREAKING CHANGE: The Promotion entity is now translatable, which means existing promotions will need
to be migrated to the new DB schema and care taken to preserve the name data. Also the GraphQL
API for creating and updating Promotions, as well as the corresponding PromotionService methods
have changed to take a `translations` array for setting the `name` and `description` in a given
language.
Michael Bromley пре 2 година
родитељ
комит
dada24398d
25 измењених фајлова са 338 додато и 72 уклоњено
  1. 23 2
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  2. 12 0
      packages/common/src/generated-shop-types.ts
  3. 24 2
      packages/common/src/generated-types.ts
  4. 2 1
      packages/core/e2e/active-order-strategy.e2e-spec.ts
  5. 2 1
      packages/core/e2e/draft-order.e2e-spec.ts
  6. 23 2
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  7. 11 0
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  8. 7 7
      packages/core/e2e/order-modification.e2e-spec.ts
  9. 11 4
      packages/core/e2e/order-promotion.e2e-spec.ts
  10. 13 2
      packages/core/e2e/promotion.e2e-spec.ts
  11. 10 2
      packages/core/src/api/resolvers/entity/order-entity.resolver.ts
  12. 8 2
      packages/core/src/api/schema/admin-api/promotion.api.graphql
  13. 12 0
      packages/core/src/api/schema/common/promotion.type.graphql
  14. 1 0
      packages/core/src/entity/custom-entity-fields.ts
  15. 2 0
      packages/core/src/entity/entities.ts
  16. 30 0
      packages/core/src/entity/promotion/promotion-translation.entity.ts
  17. 15 3
      packages/core/src/entity/promotion/promotion.entity.ts
  18. 1 1
      packages/core/src/service/services/order.service.ts
  19. 62 39
      packages/core/src/service/services/promotion.service.ts
  20. 23 2
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  21. 23 2
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  22. 11 0
      packages/payments-plugin/e2e/graphql/generated-shop-types.ts
  23. 12 0
      packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts
  24. 0 0
      schema-admin.json
  25. 0 0
      schema-shop.json

+ 23 - 2
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -790,9 +790,9 @@ export type CreatePromotionInput = {
     customFields?: InputMaybe<Scalars['JSON']>;
     customFields?: InputMaybe<Scalars['JSON']>;
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 };
 
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
 export type PromotionFilterParameter = {
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id?: InputMaybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 };
 
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
 export type UpdatePromotionResult = MissingConditionsError | Promotion;

+ 12 - 0
packages/common/src/generated-shop-types.ts

@@ -2696,12 +2696,14 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
@@ -2711,6 +2713,16 @@ export type PromotionList = PaginatedList & {
     totalItems: Scalars['Int'];
     totalItems: Scalars['Int'];
 };
 };
 
 
+export type PromotionTranslation = {
+    __typename?: 'PromotionTranslation';
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
 export type Query = {
 export type Query = {
     __typename?: 'Query';
     __typename?: 'Query';
     /** The active Channel */
     /** The active Channel */

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

@@ -804,9 +804,9 @@ export type CreatePromotionInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   customFields?: InputMaybe<Scalars['JSON']>;
   enabled: Scalars['Boolean'];
   enabled: Scalars['Boolean'];
   endsAt?: InputMaybe<Scalars['DateTime']>;
   endsAt?: InputMaybe<Scalars['DateTime']>;
-  name: Scalars['String'];
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslationInput>;
 };
 };
 
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4423,18 +4423,21 @@ export type Promotion = Node & {
   couponCode?: Maybe<Scalars['String']>;
   couponCode?: Maybe<Scalars['String']>;
   createdAt: Scalars['DateTime'];
   createdAt: Scalars['DateTime'];
   customFields?: Maybe<Scalars['JSON']>;
   customFields?: Maybe<Scalars['JSON']>;
+  description: Scalars['String'];
   enabled: Scalars['Boolean'];
   enabled: Scalars['Boolean'];
   endsAt?: Maybe<Scalars['DateTime']>;
   endsAt?: Maybe<Scalars['DateTime']>;
   id: Scalars['ID'];
   id: Scalars['ID'];
   name: Scalars['String'];
   name: Scalars['String'];
   perCustomerUsageLimit?: Maybe<Scalars['Int']>;
   perCustomerUsageLimit?: Maybe<Scalars['Int']>;
   startsAt?: Maybe<Scalars['DateTime']>;
   startsAt?: Maybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslation>;
   updatedAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
 };
 };
 
 
 export type PromotionFilterParameter = {
 export type PromotionFilterParameter = {
   couponCode?: InputMaybe<StringOperators>;
   couponCode?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
   createdAt?: InputMaybe<DateOperators>;
+  description?: InputMaybe<StringOperators>;
   enabled?: InputMaybe<BooleanOperators>;
   enabled?: InputMaybe<BooleanOperators>;
   endsAt?: InputMaybe<DateOperators>;
   endsAt?: InputMaybe<DateOperators>;
   id?: InputMaybe<IdOperators>;
   id?: InputMaybe<IdOperators>;
@@ -4466,6 +4469,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
 export type PromotionSortParameter = {
   couponCode?: InputMaybe<SortOrder>;
   couponCode?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
+  description?: InputMaybe<SortOrder>;
   endsAt?: InputMaybe<SortOrder>;
   endsAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
@@ -4474,6 +4478,24 @@ export type PromotionSortParameter = {
   updatedAt?: InputMaybe<SortOrder>;
   updatedAt?: InputMaybe<SortOrder>;
 };
 };
 
 
+export type PromotionTranslation = {
+  __typename?: 'PromotionTranslation';
+  createdAt: Scalars['DateTime'];
+  description: Scalars['String'];
+  id: Scalars['ID'];
+  languageCode: LanguageCode;
+  name: Scalars['String'];
+  updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  id?: InputMaybe<Scalars['ID']>;
+  languageCode: LanguageCode;
+  name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
 export type QuantityTooGreatError = ErrorResult & {
   __typename?: 'QuantityTooGreatError';
   __typename?: 'QuantityTooGreatError';
@@ -5802,9 +5824,9 @@ export type UpdatePromotionInput = {
   enabled?: InputMaybe<Scalars['Boolean']>;
   enabled?: InputMaybe<Scalars['Boolean']>;
   endsAt?: InputMaybe<Scalars['DateTime']>;
   endsAt?: InputMaybe<Scalars['DateTime']>;
   id: Scalars['ID'];
   id: Scalars['ID'];
-  name?: InputMaybe<Scalars['String']>;
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 };
 
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
 export type UpdatePromotionResult = MissingConditionsError | Promotion;

+ 2 - 1
packages/core/e2e/active-order-strategy.e2e-spec.ts

@@ -1,3 +1,4 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { mergeConfig, orderPercentageDiscount } from '@vendure/core';
 import { mergeConfig, orderPercentageDiscount } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
@@ -148,7 +149,6 @@ describe('custom ActiveOrderStrategy', () => {
                 {
                 {
                     input: {
                     input: {
                         enabled: true,
                         enabled: true,
-                        name: 'Free with test coupon',
                         couponCode: TEST_COUPON_CODE,
                         couponCode: TEST_COUPON_CODE,
                         conditions: [],
                         conditions: [],
                         actions: [
                         actions: [
@@ -157,6 +157,7 @@ describe('custom ActiveOrderStrategy', () => {
                                 arguments: [{ name: 'discount', value: '100' }],
                                 arguments: [{ name: 'discount', value: '100' }],
                             },
                             },
                         ],
                         ],
+                        translations: [{ languageCode: LanguageCode.en, name: 'Free with test coupon' }],
                     },
                     },
                 },
                 },
             );
             );

+ 2 - 1
packages/core/e2e/draft-order.e2e-spec.ts

@@ -1,4 +1,5 @@
 /* tslint:disable:no-non-null-assertion */
 /* tslint:disable:no-non-null-assertion */
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { DefaultLogger, mergeConfig, orderPercentageDiscount } from '@vendure/core';
 import { DefaultLogger, mergeConfig, orderPercentageDiscount } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
@@ -72,7 +73,6 @@ describe('Draft Orders resolver', () => {
             Codegen.CreatePromotionMutationVariables
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
         >(CREATE_PROMOTION, {
             input: {
             input: {
-                name: 'Free Order',
                 enabled: true,
                 enabled: true,
                 conditions: [],
                 conditions: [],
                 couponCode: freeOrderCouponCode,
                 couponCode: freeOrderCouponCode,
@@ -82,6 +82,7 @@ describe('Draft Orders resolver', () => {
                         arguments: [{ name: 'discount', value: '100' }],
                         arguments: [{ name: 'discount', value: '100' }],
                     },
                     },
                 ],
                 ],
+                translations: [{ languageCode: LanguageCode.en, name: 'Free Order' }],
             },
             },
         });
         });
     }, TEST_SETUP_TIMEOUT_MS);
     }, TEST_SETUP_TIMEOUT_MS);

+ 23 - 2
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -790,9 +790,9 @@ export type CreatePromotionInput = {
     customFields?: InputMaybe<Scalars['JSON']>;
     customFields?: InputMaybe<Scalars['JSON']>;
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 };
 
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
 export type PromotionFilterParameter = {
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id?: InputMaybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 };
 
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
 export type UpdatePromotionResult = MissingConditionsError | Promotion;

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

@@ -2604,12 +2604,14 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
@@ -2618,6 +2620,15 @@ export type PromotionList = PaginatedList & {
     totalItems: Scalars['Int'];
     totalItems: Scalars['Int'];
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
 export type Query = {
 export type Query = {
     /** The active Channel */
     /** The active Channel */
     activeChannel: Channel;
     activeChannel: Channel;

+ 7 - 7
packages/core/e2e/order-modification.e2e-spec.ts

@@ -1307,7 +1307,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
             >(CREATE_PROMOTION, {
                 input: {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF',
                     couponCode: '5OFF',
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
@@ -1317,6 +1316,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                         },
                     ],
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
                 },
             });
             });
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
@@ -1456,7 +1456,6 @@ describe('Order modification', () => {
             CREATE_PROMOTION,
             CREATE_PROMOTION,
             {
             {
                 input: {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF',
                     couponCode: '5OFF',
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
@@ -1466,6 +1465,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                         },
                     ],
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
                 },
             },
             },
         );
         );
@@ -1571,7 +1571,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
             >(CREATE_PROMOTION, {
                 input: {
                 input: {
-                    name: 'half price',
                     couponCode: 'HALF',
                     couponCode: 'HALF',
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
@@ -1584,6 +1583,7 @@ describe('Order modification', () => {
                             ],
                             ],
                         },
                         },
                     ],
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: 'half price' }],
                 },
                 },
             });
             });
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
@@ -1618,7 +1618,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
             >(CREATE_PROMOTION, {
                 input: {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF2',
                     couponCode: '5OFF2',
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
@@ -1628,6 +1627,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                         },
                     ],
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
                 },
             });
             });
 
 
@@ -1686,7 +1686,6 @@ describe('Order modification', () => {
                     Codegen.CreatePromotionMutationVariables
                     Codegen.CreatePromotionMutationVariables
                 >(CREATE_PROMOTION, {
                 >(CREATE_PROMOTION, {
                     input: {
                     input: {
-                        name: '50 off orders over 100',
                         enabled: true,
                         enabled: true,
                         conditions: [
                         conditions: [
                             {
                             {
@@ -1703,6 +1702,7 @@ describe('Order modification', () => {
                                 arguments: [{ name: 'discount', value: JSON.stringify(promoDiscount) }],
                                 arguments: [{ name: 'discount', value: JSON.stringify(promoDiscount) }],
                             },
                             },
                         ],
                         ],
+                        translations: [{ languageCode: LanguageCode.en, name: '50 off orders over 100' }],
                     },
                     },
                 });
                 });
                 promoId = (createPromotion as any).id;
                 promoId = (createPromotion as any).id;
@@ -2097,7 +2097,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
             >(CREATE_PROMOTION, {
                 input: {
                 input: {
-                    name: '50% off',
                     couponCode: CODE_50PC_OFF,
                     couponCode: CODE_50PC_OFF,
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
@@ -2107,6 +2106,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '50' }],
                             arguments: [{ name: 'discount', value: '50' }],
                         },
                         },
                     ],
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '50% off' }],
                 },
                 },
             });
             });
             await adminClient.query<
             await adminClient.query<
@@ -2114,11 +2114,11 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
             >(CREATE_PROMOTION, {
                 input: {
                 input: {
-                    name: 'Free shipping',
                     couponCode: CODE_FREE_SHIPPING,
                     couponCode: CODE_FREE_SHIPPING,
                     enabled: true,
                     enabled: true,
                     conditions: [],
                     conditions: [],
                     actions: [{ code: freeShipping.code, arguments: [] }],
                     actions: [{ code: freeShipping.code, arguments: [] }],
+                    translations: [{ languageCode: LanguageCode.en, name: 'Free shipping' }],
                 },
                 },
             });
             });
 
 

+ 11 - 4
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -317,8 +317,8 @@ describe('Promotions applied to Orders', () => {
 
 
             beforeAll(async () => {
             beforeAll(async () => {
                 const { createChannel } = await adminClient.query<
                 const { createChannel } = await adminClient.query<
-                    CreateChannel.Mutation,
-                    CreateChannel.Variables
+                    Codegen.CreateChannelMutation,
+                    Codegen.CreateChannelMutationVariables
                 >(CREATE_CHANNEL, {
                 >(CREATE_CHANNEL, {
                     input: {
                     input: {
                         code: 'other-channel',
                         code: 'other-channel',
@@ -1758,12 +1758,19 @@ describe('Promotions applied to Orders', () => {
         await deletePromotion(deletedPromotion.id);
         await deletePromotion(deletedPromotion.id);
     }
     }
 
 
-    async function createPromotion(input: Codegen.CreatePromotionInput): Promise<Codegen.PromotionFragment> {
+    async function createPromotion(
+        input: Omit<Codegen.CreatePromotionInput, 'translations'> & { name: string },
+    ): Promise<Codegen.PromotionFragment> {
+        const correctedInput = {
+            ...input,
+            translations: [{ languageCode: LanguageCode.en, name: input.name }],
+        };
+        delete (correctedInput as any).name;
         const result = await adminClient.query<
         const result = await adminClient.query<
             Codegen.CreatePromotionMutation,
             Codegen.CreatePromotionMutation,
             Codegen.CreatePromotionMutationVariables
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
         >(CREATE_PROMOTION, {
-            input,
+            input: correctedInput,
         });
         });
         return result.createPromotion as Codegen.PromotionFragment;
         return result.createPromotion as Codegen.PromotionFragment;
     }
     }

+ 13 - 2
packages/core/e2e/promotion.e2e-spec.ts

@@ -73,11 +73,17 @@ describe('Promotion resolver', () => {
             Codegen.CreatePromotionMutationVariables
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
         >(CREATE_PROMOTION, {
             input: {
             input: {
-                name: 'test promotion',
                 enabled: true,
                 enabled: true,
                 couponCode: 'TEST123',
                 couponCode: 'TEST123',
                 startsAt: new Date('2019-10-30T00:00:00.000Z'),
                 startsAt: new Date('2019-10-30T00:00:00.000Z'),
                 endsAt: new Date('2019-12-01T00:00:00.000Z'),
                 endsAt: new Date('2019-12-01T00:00:00.000Z'),
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'test promotion',
+                        description: 'a test promotion',
+                    },
+                ],
                 conditions: [
                 conditions: [
                     {
                     {
                         code: promoCondition.code,
                         code: promoCondition.code,
@@ -109,8 +115,13 @@ describe('Promotion resolver', () => {
             Codegen.CreatePromotionMutationVariables
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
         >(CREATE_PROMOTION, {
             input: {
             input: {
-                name: 'bad promotion',
                 enabled: true,
                 enabled: true,
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'bad promotion',
+                    },
+                ],
                 conditions: [],
                 conditions: [],
                 actions: [
                 actions: [
                     {
                     {

+ 10 - 2
packages/core/src/api/resolvers/entity/order-entity.resolver.ts

@@ -4,6 +4,7 @@ import { HistoryEntryListOptions, OrderHistoryArgs, SortOrder } from '@vendure/c
 import { assertFound, idsAreEqual } from '../../../common/utils';
 import { assertFound, idsAreEqual } from '../../../common/utils';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
+import { TranslatorService } from '../../../service/index';
 import { HistoryService } from '../../../service/services/history.service';
 import { HistoryService } from '../../../service/services/history.service';
 import { OrderService } from '../../../service/services/order.service';
 import { OrderService } from '../../../service/services/order.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
@@ -18,6 +19,7 @@ export class OrderEntityResolver {
         private orderService: OrderService,
         private orderService: OrderService,
         private shippingMethodService: ShippingMethodService,
         private shippingMethodService: ShippingMethodService,
         private historyService: HistoryService,
         private historyService: HistoryService,
+        private translator: TranslatorService,
     ) {}
     ) {}
 
 
     @ResolveField()
     @ResolveField()
@@ -70,8 +72,14 @@ export class OrderEntityResolver {
 
 
     @ResolveField()
     @ResolveField()
     async promotions(@Ctx() ctx: RequestContext, @Parent() order: Order) {
     async promotions(@Ctx() ctx: RequestContext, @Parent() order: Order) {
-        if (order.promotions) {
-            return order.promotions;
+        // If the order has been hydrated with the promotions, then we can just return those
+        // as long as they have the translations joined.
+        if (
+            order.promotions &&
+            (order.promotions.length === 0 ||
+                (order.promotions.length > 0 && order.promotions[0].translations))
+        ) {
+            return order.promotions.map(p => this.translator.translate(p, ctx));
         }
         }
         return this.orderService.getOrderPromotions(ctx, order.id);
         return this.orderService.getOrderPromotions(ctx, order.id);
     }
     }

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

@@ -18,8 +18,13 @@ type Mutation {
 # generated by generateListOptions function
 # generated by generateListOptions function
 input PromotionListOptions
 input PromotionListOptions
 
 
+input PromotionTranslationInput {
+    id: ID
+    languageCode: LanguageCode!
+    name: String
+    description: String
+}
 input CreatePromotionInput {
 input CreatePromotionInput {
-    name: String!
     enabled: Boolean!
     enabled: Boolean!
     startsAt: DateTime
     startsAt: DateTime
     endsAt: DateTime
     endsAt: DateTime
@@ -27,11 +32,11 @@ input CreatePromotionInput {
     perCustomerUsageLimit: Int
     perCustomerUsageLimit: Int
     conditions: [ConfigurableOperationInput!]!
     conditions: [ConfigurableOperationInput!]!
     actions: [ConfigurableOperationInput!]!
     actions: [ConfigurableOperationInput!]!
+    translations: [PromotionTranslationInput!]!
 }
 }
 
 
 input UpdatePromotionInput {
 input UpdatePromotionInput {
     id: ID!
     id: ID!
-    name: String
     enabled: Boolean
     enabled: Boolean
     startsAt: DateTime
     startsAt: DateTime
     endsAt: DateTime
     endsAt: DateTime
@@ -39,6 +44,7 @@ input UpdatePromotionInput {
     perCustomerUsageLimit: Int
     perCustomerUsageLimit: Int
     conditions: [ConfigurableOperationInput!]
     conditions: [ConfigurableOperationInput!]
     actions: [ConfigurableOperationInput!]
     actions: [ConfigurableOperationInput!]
+    translations: [PromotionTranslationInput!]
 }
 }
 
 
 input AssignPromotionsToChannelInput {
 input AssignPromotionsToChannelInput {

+ 12 - 0
packages/core/src/api/schema/common/promotion.type.graphql

@@ -7,11 +7,23 @@ type Promotion implements Node {
     couponCode: String
     couponCode: String
     perCustomerUsageLimit: Int
     perCustomerUsageLimit: Int
     name: String!
     name: String!
+    description: String!
     enabled: Boolean!
     enabled: Boolean!
     conditions: [ConfigurableOperation!]!
     conditions: [ConfigurableOperation!]!
     actions: [ConfigurableOperation!]!
     actions: [ConfigurableOperation!]!
+    translations: [PromotionTranslation!]!
 }
 }
 
 
+type PromotionTranslation {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    languageCode: LanguageCode!
+    name: String!
+    description: String!
+}
+
+
 type PromotionList implements PaginatedList {
 type PromotionList implements PaginatedList {
     items: [Promotion!]!
     items: [Promotion!]!
     totalItems: Int!
     totalItems: Int!

+ 1 - 0
packages/core/src/entity/custom-entity-fields.ts

@@ -27,6 +27,7 @@ export class CustomProductOptionGroupFieldsTranslation {}
 export class CustomProductVariantFields {}
 export class CustomProductVariantFields {}
 export class CustomProductVariantFieldsTranslation {}
 export class CustomProductVariantFieldsTranslation {}
 export class CustomPromotionFields {}
 export class CustomPromotionFields {}
+export class CustomPromotionFieldsTranslation {}
 export class CustomSellerFields {}
 export class CustomSellerFields {}
 export class CustomShippingMethodFields {}
 export class CustomShippingMethodFields {}
 export class CustomShippingMethodFieldsTranslation {}
 export class CustomShippingMethodFieldsTranslation {}

+ 2 - 0
packages/core/src/entity/entities.ts

@@ -42,6 +42,7 @@ import { ProductVariant } from './product-variant/product-variant.entity';
 import { ProductAsset } from './product/product-asset.entity';
 import { ProductAsset } from './product/product-asset.entity';
 import { ProductTranslation } from './product/product-translation.entity';
 import { ProductTranslation } from './product/product-translation.entity';
 import { Product } from './product/product.entity';
 import { Product } from './product/product.entity';
+import { PromotionTranslation } from './promotion/promotion-translation.entity';
 import { Promotion } from './promotion/promotion.entity';
 import { Promotion } from './promotion/promotion.entity';
 import { Refund } from './refund/refund.entity';
 import { Refund } from './refund/refund.entity';
 import { Role } from './role/role.entity';
 import { Role } from './role/role.entity';
@@ -119,6 +120,7 @@ export const coreEntitiesMap = {
     ProductVariantPrice,
     ProductVariantPrice,
     ProductVariantTranslation,
     ProductVariantTranslation,
     Promotion,
     Promotion,
+    PromotionTranslation,
     Refund,
     Refund,
     RefundLine,
     RefundLine,
     Release,
     Release,

+ 30 - 0
packages/core/src/entity/promotion/promotion-translation.entity.ts

@@ -0,0 +1,30 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { Column, Entity, Index, ManyToOne } from 'typeorm';
+
+import { Translation } from '../../common/types/locale-types';
+import { HasCustomFields } from '../../config/custom-field/custom-field-types';
+import { VendureEntity } from '../base/base.entity';
+import { CustomPromotionFieldsTranslation } from '../custom-entity-fields';
+
+import { Promotion } from './promotion.entity';
+
+@Entity()
+export class PromotionTranslation extends VendureEntity implements Translation<Promotion>, HasCustomFields {
+    constructor(input?: DeepPartial<Translation<Promotion>>) {
+        super(input);
+    }
+
+    @Column('varchar') languageCode: LanguageCode;
+
+    @Column() name: string;
+
+    @Column('text', { default: '' }) description: string;
+
+    @Index()
+    @ManyToOne(type => Promotion, base => base.translations, { onDelete: 'CASCADE' })
+    base: Promotion;
+
+    @Column(type => CustomPromotionFieldsTranslation)
+    customFields: CustomPromotionFieldsTranslation;
+}

+ 15 - 3
packages/core/src/entity/promotion/promotion.entity.ts

@@ -1,11 +1,12 @@
 import { Adjustment, AdjustmentType, ConfigurableOperation } from '@vendure/common/lib/generated-types';
 import { Adjustment, AdjustmentType, ConfigurableOperation } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToMany, OneToMany } from 'typeorm';
 
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContext } from '../../api/common/request-context';
 import { roundMoney } from '../../common/round-money';
 import { roundMoney } from '../../common/round-money';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
+import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
 import { getConfig } from '../../config/config-helpers';
 import { getConfig } from '../../config/config-helpers';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import {
 import {
@@ -19,8 +20,11 @@ import { Channel } from '../channel/channel.entity';
 import { CustomPromotionFields } from '../custom-entity-fields';
 import { CustomPromotionFields } from '../custom-entity-fields';
 import { OrderLine } from '../order-line/order-line.entity';
 import { OrderLine } from '../order-line/order-line.entity';
 import { Order } from '../order/order.entity';
 import { Order } from '../order/order.entity';
+import { PaymentMethodTranslation } from '../payment-method/payment-method-translation.entity';
 import { ShippingLine } from '../shipping-line/shipping-line.entity';
 import { ShippingLine } from '../shipping-line/shipping-line.entity';
 
 
+import { PromotionTranslation } from './promotion-translation.entity';
+
 export interface ApplyOrderItemActionArgs {
 export interface ApplyOrderItemActionArgs {
     orderLine: OrderLine;
     orderLine: OrderLine;
 }
 }
@@ -51,7 +55,10 @@ export type PromotionTestResult = boolean | PromotionState;
  * @docsCategory entities
  * @docsCategory entities
  */
  */
 @Entity()
 @Entity()
-export class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable, HasCustomFields {
+export class Promotion
+    extends AdjustmentSource
+    implements ChannelAware, SoftDeletable, HasCustomFields, Translatable
+{
     type = AdjustmentType.PROMOTION;
     type = AdjustmentType.PROMOTION;
     private readonly allConditions: { [code: string]: PromotionCondition } = {};
     private readonly allConditions: { [code: string]: PromotionCondition } = {};
     private readonly allActions: {
     private readonly allActions: {
@@ -88,7 +95,12 @@ export class Promotion extends AdjustmentSource implements ChannelAware, SoftDel
     @Column({ nullable: true })
     @Column({ nullable: true })
     perCustomerUsageLimit: number;
     perCustomerUsageLimit: number;
 
 
-    @Column() name: string;
+    name: LocaleString;
+
+    description: LocaleString;
+
+    @OneToMany(type => PromotionTranslation, translation => translation.base, { eager: true })
+    translations: Array<Translation<Promotion>>;
 
 
     @Column() enabled: boolean;
     @Column() enabled: boolean;
 
 

+ 1 - 1
packages/core/src/service/services/order.service.ts

@@ -767,7 +767,7 @@ export class OrderService {
             channelId: ctx.channelId,
             channelId: ctx.channelId,
             relations: ['promotions'],
             relations: ['promotions'],
         });
         });
-        return order.promotions || [];
+        return order.promotions.map(p => this.translator.translate(p, ctx)) || [];
     }
     }
 
 
     /**
     /**

+ 62 - 39
packages/core/src/service/services/promotion.service.ts

@@ -34,6 +34,7 @@ import { PromotionAction } from '../../config/promotion/promotion-action';
 import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { Order } from '../../entity/order/order.entity';
 import { Order } from '../../entity/order/order.entity';
+import { PromotionTranslation } from '../../entity/promotion/promotion-translation.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { EventBus } from '../../event-bus';
 import { EventBus } from '../../event-bus';
 import { PromotionEvent } from '../../event-bus/events/promotion-event';
 import { PromotionEvent } from '../../event-bus/events/promotion-event';
@@ -41,6 +42,8 @@ import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderState } from '../helpers/order-state-machine/order-state';
 import { OrderState } from '../helpers/order-state-machine/order-state';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
 import { ChannelService } from './channel.service';
 import { ChannelService } from './channel.service';
@@ -64,6 +67,8 @@ export class PromotionService {
         private configArgService: ConfigArgService,
         private configArgService: ConfigArgService,
         private customFieldRelationService: CustomFieldRelationService,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
         private eventBus: EventBus,
+        private translatableSaver: TranslatableSaver,
+        private translator: TranslatorService,
     ) {
     ) {
         this.availableConditions = this.configService.promotionOptions.promotionConditions || [];
         this.availableConditions = this.configService.promotionOptions.promotionConditions || [];
         this.availableActions = this.configService.promotionOptions.promotionActions || [];
         this.availableActions = this.configService.promotionOptions.promotionActions || [];
@@ -82,10 +87,13 @@ export class PromotionService {
                 ctx,
                 ctx,
             })
             })
             .getManyAndCount()
             .getManyAndCount()
-            .then(([items, totalItems]) => ({
-                items,
-                totalItems,
-            }));
+            .then(([promotions, totalItems]) => {
+                const items = promotions.map(promotion => this.translator.translate(promotion, ctx));
+                return {
+                    items,
+                    totalItems,
+                };
+            });
     }
     }
 
 
     async findOne(
     async findOne(
@@ -93,10 +101,12 @@ export class PromotionService {
         adjustmentSourceId: ID,
         adjustmentSourceId: ID,
         relations: RelationPaths<Promotion> = [],
         relations: RelationPaths<Promotion> = [],
     ): Promise<Promotion | undefined> {
     ): Promise<Promotion | undefined> {
-        return this.connection.findOneInChannel(ctx, Promotion, adjustmentSourceId, ctx.channelId, {
-            where: { deletedAt: null },
-            relations,
-        });
+        return this.connection
+            .findOneInChannel(ctx, Promotion, adjustmentSourceId, ctx.channelId, {
+                where: { deletedAt: null },
+                relations,
+            })
+            .then(promotion => promotion && this.translator.translate(promotion, ctx));
     }
     }
 
 
     getPromotionConditions(ctx: RequestContext): ConfigurableOperationDefinition[] {
     getPromotionConditions(ctx: RequestContext): ConfigurableOperationDefinition[] {
@@ -116,22 +126,21 @@ export class PromotionService {
         );
         );
         const actions = input.actions.map(a => this.configArgService.parseInput('PromotionAction', a));
         const actions = input.actions.map(a => this.configArgService.parseInput('PromotionAction', a));
         this.validateRequiredConditions(conditions, actions);
         this.validateRequiredConditions(conditions, actions);
-        const promotion = new Promotion({
-            name: input.name,
-            enabled: input.enabled,
-            couponCode: input.couponCode,
-            perCustomerUsageLimit: input.perCustomerUsageLimit,
-            startsAt: input.startsAt,
-            endsAt: input.endsAt,
-            conditions,
-            actions,
-            priorityScore: this.calculatePriorityScore(input),
-        });
-        if (promotion.conditions.length === 0 && !promotion.couponCode) {
+        if (conditions.length === 0 && !input.couponCode) {
             return new MissingConditionsError();
             return new MissingConditionsError();
         }
         }
-        await this.channelService.assignToCurrentChannel(promotion, ctx);
-        const newPromotion = await this.connection.getRepository(ctx, Promotion).save(promotion);
+        const newPromotion = await this.translatableSaver.create({
+            ctx,
+            input,
+            entityType: Promotion,
+            translationType: PromotionTranslation,
+            beforeSave: async p => {
+                p.priorityScore = this.calculatePriorityScore(input);
+                p.conditions = conditions;
+                p.actions = actions;
+                await this.channelService.assignToCurrentChannel(p, ctx);
+            },
+        });
         const promotionWithRelations = await this.customFieldRelationService.updateRelations(
         const promotionWithRelations = await this.customFieldRelationService.updateRelations(
             ctx,
             ctx,
             Promotion,
             Promotion,
@@ -149,22 +158,34 @@ export class PromotionService {
         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,
         });
         });
-        const updatedPromotion = patchEntity(promotion, omit(input, ['conditions', 'actions']));
-        if (input.conditions) {
-            updatedPromotion.conditions = input.conditions.map(c =>
-                this.configArgService.parseInput('PromotionCondition', c),
-            );
-        }
-        if (input.actions) {
-            updatedPromotion.actions = input.actions.map(a =>
-                this.configArgService.parseInput('PromotionAction', a),
-            );
-        }
-        if (promotion.conditions.length === 0 && !promotion.couponCode) {
+
+        const hasConditions = input.conditions
+            ? input.conditions.length > 0
+            : promotion.conditions.length > 0;
+        const hasCouponCode = input.couponCode != null ? !!input.couponCode : !!promotion.couponCode;
+        if (!hasConditions && !hasCouponCode) {
             return new MissingConditionsError();
             return new MissingConditionsError();
         }
         }
-        promotion.priorityScore = this.calculatePriorityScore(input);
-        await this.connection.getRepository(ctx, Promotion).save(updatedPromotion, { reload: false });
+        const updatedPromotion = await this.translatableSaver.update({
+            ctx,
+            input,
+            entityType: Promotion,
+            translationType: PromotionTranslation,
+            beforeSave: async p => {
+                p.priorityScore = this.calculatePriorityScore(input);
+                if (input.conditions) {
+                    p.conditions = input.conditions.map(c =>
+                        this.configArgService.parseInput('PromotionCondition', c),
+                    );
+                }
+                if (input.actions) {
+                    p.actions = input.actions.map(a =>
+                        this.configArgService.parseInput('PromotionAction', a),
+                    );
+                }
+                await this.channelService.assignToCurrentChannel(p, ctx);
+            },
+        });
         await this.customFieldRelationService.updateRelations(ctx, Promotion, input, updatedPromotion);
         await this.customFieldRelationService.updateRelations(ctx, Promotion, input, updatedPromotion);
         this.eventBus.publish(new PromotionEvent(ctx, promotion, 'updated', input));
         this.eventBus.publish(new PromotionEvent(ctx, promotion, 'updated', input));
         return assertFound(this.findOne(ctx, updatedPromotion.id));
         return assertFound(this.findOne(ctx, updatedPromotion.id));
@@ -200,7 +221,7 @@ export class PromotionService {
         for (const promotion of promotions) {
         for (const promotion of promotions) {
             await this.channelService.assignToChannels(ctx, Promotion, promotion.id, [input.channelId]);
             await this.channelService.assignToChannels(ctx, Promotion, promotion.id, [input.channelId]);
         }
         }
-        return promotions;
+        return promotions.map(p => this.translator.translate(p, ctx));
     }
     }
 
 
     async removePromotionsFromChannel(ctx: RequestContext, input: RemovePromotionsFromChannelInput) {
     async removePromotionsFromChannel(ctx: RequestContext, input: RemovePromotionsFromChannelInput) {
@@ -218,7 +239,7 @@ export class PromotionService {
         for (const promotion of promotions) {
         for (const promotion of promotions) {
             await this.channelService.removeFromChannels(ctx, Promotion, promotion.id, [input.channelId]);
             await this.channelService.removeFromChannels(ctx, Promotion, promotion.id, [input.channelId]);
         }
         }
-        return promotions;
+        return promotions.map(p => this.translator.translate(p, ctx));
     }
     }
 
 
     /**
     /**
@@ -264,11 +285,13 @@ export class PromotionService {
             .getRepository(ctx, Promotion)
             .getRepository(ctx, Promotion)
             .createQueryBuilder('promotion')
             .createQueryBuilder('promotion')
             .leftJoin('promotion.channels', 'channel')
             .leftJoin('promotion.channels', 'channel')
+            .leftJoinAndSelect('promotion.translations', 'translation')
             .where('channel.id = :channelId', { channelId: ctx.channelId })
             .where('channel.id = :channelId', { channelId: ctx.channelId })
             .andWhere('promotion.deletedAt IS NULL')
             .andWhere('promotion.deletedAt IS NULL')
             .andWhere('promotion.enabled = :enabled', { enabled: true })
             .andWhere('promotion.enabled = :enabled', { enabled: true })
             .orderBy('promotion.priorityScore', 'ASC')
             .orderBy('promotion.priorityScore', 'ASC')
-            .getMany();
+            .getMany()
+            .then(promotions => promotions.map(p => this.translator.translate(p, ctx)));
     }
     }
 
 
     async getActivePromotionsOnOrder(ctx: RequestContext, orderId: ID): Promise<Promotion[]> {
     async getActivePromotionsOnOrder(ctx: RequestContext, orderId: ID): Promise<Promotion[]> {

+ 23 - 2
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -790,9 +790,9 @@ export type CreatePromotionInput = {
     customFields?: InputMaybe<Scalars['JSON']>;
     customFields?: InputMaybe<Scalars['JSON']>;
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 };
 
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
 export type PromotionFilterParameter = {
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id?: InputMaybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 };
 
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
 export type UpdatePromotionResult = MissingConditionsError | Promotion;

+ 23 - 2
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -790,9 +790,9 @@ export type CreatePromotionInput = {
     customFields?: InputMaybe<Scalars['JSON']>;
     customFields?: InputMaybe<Scalars['JSON']>;
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 };
 
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
 export type PromotionFilterParameter = {
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     updatedAt?: InputMaybe<SortOrder>;
     updatedAt?: InputMaybe<SortOrder>;
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
+export type PromotionTranslationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id?: InputMaybe<Scalars['ID']>;
+    languageCode: LanguageCode;
+    name?: InputMaybe<Scalars['String']>;
+};
+
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 /** Returned if the specified quantity of an OrderLine is greater than the number of items in that line */
 export type QuantityTooGreatError = ErrorResult & {
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 };
 
 
 export type UpdatePromotionResult = MissingConditionsError | Promotion;
 export type UpdatePromotionResult = MissingConditionsError | Promotion;

+ 11 - 0
packages/payments-plugin/e2e/graphql/generated-shop-types.ts

@@ -2604,12 +2604,14 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
@@ -2618,6 +2620,15 @@ export type PromotionList = PaginatedList & {
     totalItems: Scalars['Int'];
     totalItems: Scalars['Int'];
 };
 };
 
 
+export type PromotionTranslation = {
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
 export type Query = {
 export type Query = {
     /** The active Channel */
     /** The active Channel */
     activeChannel: Channel;
     activeChannel: Channel;

+ 12 - 0
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts

@@ -2746,12 +2746,14 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     id: Scalars['ID'];
     name: Scalars['String'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
     updatedAt: Scalars['DateTime'];
 };
 };
 
 
@@ -2761,6 +2763,16 @@ export type PromotionList = PaginatedList & {
     totalItems: Scalars['Int'];
     totalItems: Scalars['Int'];
 };
 };
 
 
+export type PromotionTranslation = {
+    __typename?: 'PromotionTranslation';
+    createdAt: Scalars['DateTime'];
+    description: Scalars['String'];
+    id: Scalars['ID'];
+    languageCode: LanguageCode;
+    name: Scalars['String'];
+    updatedAt: Scalars['DateTime'];
+};
+
 export type Query = {
 export type Query = {
     __typename?: 'Query';
     __typename?: 'Query';
     /** The active Channel */
     /** The active Channel */

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
schema-admin.json


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
schema-shop.json


Неке датотеке нису приказане због велике количине промена