Parcourir la source

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 il y a 2 ans
Parent
commit
dada24398d
25 fichiers modifiés avec 338 ajouts et 72 suppressions
  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']>;
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     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 */
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 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']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
@@ -2711,6 +2713,16 @@ export type PromotionList = PaginatedList & {
     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 = {
     __typename?: 'Query';
     /** The active Channel */

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

@@ -804,9 +804,9 @@ export type CreatePromotionInput = {
   customFields?: InputMaybe<Scalars['JSON']>;
   enabled: Scalars['Boolean'];
   endsAt?: InputMaybe<Scalars['DateTime']>;
-  name: Scalars['String'];
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4423,18 +4423,21 @@ export type Promotion = Node & {
   couponCode?: Maybe<Scalars['String']>;
   createdAt: Scalars['DateTime'];
   customFields?: Maybe<Scalars['JSON']>;
+  description: Scalars['String'];
   enabled: Scalars['Boolean'];
   endsAt?: Maybe<Scalars['DateTime']>;
   id: Scalars['ID'];
   name: Scalars['String'];
   perCustomerUsageLimit?: Maybe<Scalars['Int']>;
   startsAt?: Maybe<Scalars['DateTime']>;
+  translations: Array<PromotionTranslation>;
   updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
   couponCode?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
+  description?: InputMaybe<StringOperators>;
   enabled?: InputMaybe<BooleanOperators>;
   endsAt?: InputMaybe<DateOperators>;
   id?: InputMaybe<IdOperators>;
@@ -4466,6 +4469,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
   couponCode?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
+  description?: InputMaybe<SortOrder>;
   endsAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
@@ -4474,6 +4478,24 @@ export type PromotionSortParameter = {
   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 */
 export type QuantityTooGreatError = ErrorResult & {
   __typename?: 'QuantityTooGreatError';
@@ -5802,9 +5824,9 @@ export type UpdatePromotionInput = {
   enabled?: InputMaybe<Scalars['Boolean']>;
   endsAt?: InputMaybe<Scalars['DateTime']>;
   id: Scalars['ID'];
-  name?: InputMaybe<Scalars['String']>;
   perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
   startsAt?: InputMaybe<Scalars['DateTime']>;
+  translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 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 { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
@@ -148,7 +149,6 @@ describe('custom ActiveOrderStrategy', () => {
                 {
                     input: {
                         enabled: true,
-                        name: 'Free with test coupon',
                         couponCode: TEST_COUPON_CODE,
                         conditions: [],
                         actions: [
@@ -157,6 +157,7 @@ describe('custom ActiveOrderStrategy', () => {
                                 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 */
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { DefaultLogger, mergeConfig, orderPercentageDiscount } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
@@ -72,7 +73,6 @@ describe('Draft Orders resolver', () => {
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
             input: {
-                name: 'Free Order',
                 enabled: true,
                 conditions: [],
                 couponCode: freeOrderCouponCode,
@@ -82,6 +82,7 @@ describe('Draft Orders resolver', () => {
                         arguments: [{ name: 'discount', value: '100' }],
                     },
                 ],
+                translations: [{ languageCode: LanguageCode.en, name: 'Free Order' }],
             },
         });
     }, 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']>;
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     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 */
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 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']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
@@ -2618,6 +2620,15 @@ export type PromotionList = PaginatedList & {
     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 = {
     /** The active Channel */
     activeChannel: Channel;

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

@@ -1307,7 +1307,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF',
                     enabled: true,
                     conditions: [],
@@ -1317,6 +1316,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
             });
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
@@ -1456,7 +1456,6 @@ describe('Order modification', () => {
             CREATE_PROMOTION,
             {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF',
                     enabled: true,
                     conditions: [],
@@ -1466,6 +1465,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
             },
         );
@@ -1571,7 +1571,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
                 input: {
-                    name: 'half price',
                     couponCode: 'HALF',
                     enabled: true,
                     conditions: [],
@@ -1584,6 +1583,7 @@ describe('Order modification', () => {
                             ],
                         },
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: 'half price' }],
                 },
             });
             await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
@@ -1618,7 +1618,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
                 input: {
-                    name: '$5 off',
                     couponCode: '5OFF2',
                     enabled: true,
                     conditions: [],
@@ -1628,6 +1627,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '500' }],
                         },
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '$5 off' }],
                 },
             });
 
@@ -1686,7 +1686,6 @@ describe('Order modification', () => {
                     Codegen.CreatePromotionMutationVariables
                 >(CREATE_PROMOTION, {
                     input: {
-                        name: '50 off orders over 100',
                         enabled: true,
                         conditions: [
                             {
@@ -1703,6 +1702,7 @@ describe('Order modification', () => {
                                 arguments: [{ name: 'discount', value: JSON.stringify(promoDiscount) }],
                             },
                         ],
+                        translations: [{ languageCode: LanguageCode.en, name: '50 off orders over 100' }],
                     },
                 });
                 promoId = (createPromotion as any).id;
@@ -2097,7 +2097,6 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
                 input: {
-                    name: '50% off',
                     couponCode: CODE_50PC_OFF,
                     enabled: true,
                     conditions: [],
@@ -2107,6 +2106,7 @@ describe('Order modification', () => {
                             arguments: [{ name: 'discount', value: '50' }],
                         },
                     ],
+                    translations: [{ languageCode: LanguageCode.en, name: '50% off' }],
                 },
             });
             await adminClient.query<
@@ -2114,11 +2114,11 @@ describe('Order modification', () => {
                 Codegen.CreatePromotionMutationVariables
             >(CREATE_PROMOTION, {
                 input: {
-                    name: 'Free shipping',
                     couponCode: CODE_FREE_SHIPPING,
                     enabled: true,
                     conditions: [],
                     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 () => {
                 const { createChannel } = await adminClient.query<
-                    CreateChannel.Mutation,
-                    CreateChannel.Variables
+                    Codegen.CreateChannelMutation,
+                    Codegen.CreateChannelMutationVariables
                 >(CREATE_CHANNEL, {
                     input: {
                         code: 'other-channel',
@@ -1758,12 +1758,19 @@ describe('Promotions applied to Orders', () => {
         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<
             Codegen.CreatePromotionMutation,
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
-            input,
+            input: correctedInput,
         });
         return result.createPromotion as Codegen.PromotionFragment;
     }

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

@@ -73,11 +73,17 @@ describe('Promotion resolver', () => {
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
             input: {
-                name: 'test promotion',
                 enabled: true,
                 couponCode: 'TEST123',
                 startsAt: new Date('2019-10-30T00:00:00.000Z'),
                 endsAt: new Date('2019-12-01T00:00:00.000Z'),
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'test promotion',
+                        description: 'a test promotion',
+                    },
+                ],
                 conditions: [
                     {
                         code: promoCondition.code,
@@ -109,8 +115,13 @@ describe('Promotion resolver', () => {
             Codegen.CreatePromotionMutationVariables
         >(CREATE_PROMOTION, {
             input: {
-                name: 'bad promotion',
                 enabled: true,
+                translations: [
+                    {
+                        languageCode: LanguageCode.en,
+                        name: 'bad promotion',
+                    },
+                ],
                 conditions: [],
                 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 { Order } from '../../../entity/order/order.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 { OrderService } from '../../../service/services/order.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
@@ -18,6 +19,7 @@ export class OrderEntityResolver {
         private orderService: OrderService,
         private shippingMethodService: ShippingMethodService,
         private historyService: HistoryService,
+        private translator: TranslatorService,
     ) {}
 
     @ResolveField()
@@ -70,8 +72,14 @@ export class OrderEntityResolver {
 
     @ResolveField()
     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);
     }

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

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

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

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

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

@@ -27,6 +27,7 @@ export class CustomProductOptionGroupFieldsTranslation {}
 export class CustomProductVariantFields {}
 export class CustomProductVariantFieldsTranslation {}
 export class CustomPromotionFields {}
+export class CustomPromotionFieldsTranslation {}
 export class CustomSellerFields {}
 export class CustomShippingMethodFields {}
 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 { ProductTranslation } from './product/product-translation.entity';
 import { Product } from './product/product.entity';
+import { PromotionTranslation } from './promotion/promotion-translation.entity';
 import { Promotion } from './promotion/promotion.entity';
 import { Refund } from './refund/refund.entity';
 import { Role } from './role/role.entity';
@@ -119,6 +120,7 @@ export const coreEntitiesMap = {
     ProductVariantPrice,
     ProductVariantTranslation,
     Promotion,
+    PromotionTranslation,
     Refund,
     RefundLine,
     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 { 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 { roundMoney } from '../../common/round-money';
 import { AdjustmentSource } from '../../common/types/adjustment-source';
 import { ChannelAware, SoftDeletable } from '../../common/types/common-types';
+import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
 import { getConfig } from '../../config/config-helpers';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import {
@@ -19,8 +20,11 @@ import { Channel } from '../channel/channel.entity';
 import { CustomPromotionFields } from '../custom-entity-fields';
 import { OrderLine } from '../order-line/order-line.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 { PromotionTranslation } from './promotion-translation.entity';
+
 export interface ApplyOrderItemActionArgs {
     orderLine: OrderLine;
 }
@@ -51,7 +55,10 @@ export type PromotionTestResult = boolean | PromotionState;
  * @docsCategory entities
  */
 @Entity()
-export class Promotion extends AdjustmentSource implements ChannelAware, SoftDeletable, HasCustomFields {
+export class Promotion
+    extends AdjustmentSource
+    implements ChannelAware, SoftDeletable, HasCustomFields, Translatable
+{
     type = AdjustmentType.PROMOTION;
     private readonly allConditions: { [code: string]: PromotionCondition } = {};
     private readonly allActions: {
@@ -88,7 +95,12 @@ export class Promotion extends AdjustmentSource implements ChannelAware, SoftDel
     @Column({ nullable: true })
     perCustomerUsageLimit: number;
 
-    @Column() name: string;
+    name: LocaleString;
+
+    description: LocaleString;
+
+    @OneToMany(type => PromotionTranslation, translation => translation.base, { eager: true })
+    translations: Array<Translation<Promotion>>;
 
     @Column() enabled: boolean;
 

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

@@ -767,7 +767,7 @@ export class OrderService {
             channelId: ctx.channelId,
             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 { TransactionalConnection } from '../../connection/transactional-connection';
 import { Order } from '../../entity/order/order.entity';
+import { PromotionTranslation } from '../../entity/promotion/promotion-translation.entity';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { EventBus } from '../../event-bus';
 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 { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 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 { ChannelService } from './channel.service';
@@ -64,6 +67,8 @@ export class PromotionService {
         private configArgService: ConfigArgService,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
+        private translatableSaver: TranslatableSaver,
+        private translator: TranslatorService,
     ) {
         this.availableConditions = this.configService.promotionOptions.promotionConditions || [];
         this.availableActions = this.configService.promotionOptions.promotionActions || [];
@@ -82,10 +87,13 @@ export class PromotionService {
                 ctx,
             })
             .getManyAndCount()
-            .then(([items, totalItems]) => ({
-                items,
-                totalItems,
-            }));
+            .then(([promotions, totalItems]) => {
+                const items = promotions.map(promotion => this.translator.translate(promotion, ctx));
+                return {
+                    items,
+                    totalItems,
+                };
+            });
     }
 
     async findOne(
@@ -93,10 +101,12 @@ export class PromotionService {
         adjustmentSourceId: ID,
         relations: RelationPaths<Promotion> = [],
     ): 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[] {
@@ -116,22 +126,21 @@ export class PromotionService {
         );
         const actions = input.actions.map(a => this.configArgService.parseInput('PromotionAction', a));
         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();
         }
-        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(
             ctx,
             Promotion,
@@ -149,22 +158,34 @@ export class PromotionService {
         const promotion = await this.connection.getEntityOrThrow(ctx, Promotion, input.id, {
             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();
         }
-        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);
         this.eventBus.publish(new PromotionEvent(ctx, promotion, 'updated', input));
         return assertFound(this.findOne(ctx, updatedPromotion.id));
@@ -200,7 +221,7 @@ export class PromotionService {
         for (const promotion of promotions) {
             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) {
@@ -218,7 +239,7 @@ export class PromotionService {
         for (const promotion of promotions) {
             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)
             .createQueryBuilder('promotion')
             .leftJoin('promotion.channels', 'channel')
+            .leftJoinAndSelect('promotion.translations', 'translation')
             .where('channel.id = :channelId', { channelId: ctx.channelId })
             .andWhere('promotion.deletedAt IS NULL')
             .andWhere('promotion.enabled = :enabled', { enabled: true })
             .orderBy('promotion.priorityScore', 'ASC')
-            .getMany();
+            .getMany()
+            .then(promotions => promotions.map(p => this.translator.translate(p, ctx)));
     }
 
     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']>;
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     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 */
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 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']>;
     enabled: Scalars['Boolean'];
     endsAt?: InputMaybe<Scalars['DateTime']>;
-    name: Scalars['String'];
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslationInput>;
 };
 
 export type CreatePromotionResult = MissingConditionsError | Promotion;
@@ -4202,18 +4202,21 @@ export type Promotion = Node & {
     couponCode?: Maybe<Scalars['String']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
 export type PromotionFilterParameter = {
     couponCode?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
     enabled?: InputMaybe<BooleanOperators>;
     endsAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
@@ -4244,6 +4247,7 @@ export type PromotionListOptions = {
 export type PromotionSortParameter = {
     couponCode?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
     endsAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4252,6 +4256,23 @@ export type PromotionSortParameter = {
     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 */
 export type QuantityTooGreatError = ErrorResult & {
     errorCode: ErrorCode;
@@ -5510,9 +5531,9 @@ export type UpdatePromotionInput = {
     enabled?: InputMaybe<Scalars['Boolean']>;
     endsAt?: InputMaybe<Scalars['DateTime']>;
     id: Scalars['ID'];
-    name?: InputMaybe<Scalars['String']>;
     perCustomerUsageLimit?: InputMaybe<Scalars['Int']>;
     startsAt?: InputMaybe<Scalars['DateTime']>;
+    translations?: InputMaybe<Array<PromotionTranslationInput>>;
 };
 
 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']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
@@ -2618,6 +2620,15 @@ export type PromotionList = PaginatedList & {
     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 = {
     /** The active 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']>;
     createdAt: Scalars['DateTime'];
     customFields?: Maybe<Scalars['JSON']>;
+    description: Scalars['String'];
     enabled: Scalars['Boolean'];
     endsAt?: Maybe<Scalars['DateTime']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     perCustomerUsageLimit?: Maybe<Scalars['Int']>;
     startsAt?: Maybe<Scalars['DateTime']>;
+    translations: Array<PromotionTranslation>;
     updatedAt: Scalars['DateTime'];
 };
 
@@ -2761,6 +2763,16 @@ export type PromotionList = PaginatedList & {
     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 = {
     __typename?: 'Query';
     /** The active Channel */

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-admin.json


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
schema-shop.json


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff