فهرست منبع

feat(core): Implement order history

Relates to #118
Michael Bromley 6 سال پیش
والد
کامیت
e4927c3d42
25فایلهای تغییر یافته به همراه792 افزوده شده و 87 حذف شده
  1. 50 0
      packages/common/src/generated-shop-types.ts
  2. 82 31
      packages/common/src/generated-types.ts
  3. 111 29
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  4. 50 0
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  5. 216 5
      packages/core/e2e/order.e2e-spec.ts
  6. 15 2
      packages/core/src/api/common/id-codec.ts
  7. 11 2
      packages/core/src/api/resolvers/entity/order-entity.resolver.ts
  8. 25 0
      packages/core/src/api/schema/type/history-entry.type.graphql
  9. 2 0
      packages/core/src/api/schema/type/order.type.graphql
  10. 4 0
      packages/core/src/entity/entities.ts
  11. 21 0
      packages/core/src/entity/history-entry/history-entry.entity.ts
  12. 16 0
      packages/core/src/entity/history-entry/order-history-entry.entity.ts
  13. 2 0
      packages/core/src/entity/refund/refund.entity.ts
  14. 12 0
      packages/core/src/service/helpers/order-state-machine/order-state-machine.ts
  15. 14 1
      packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts
  16. 4 1
      packages/core/src/service/helpers/payment-state-machine/payment-state.ts
  17. 15 1
      packages/core/src/service/helpers/refund-state-machine/refund-state-machine.ts
  18. 6 4
      packages/core/src/service/service.module.ts
  19. 9 0
      packages/core/src/service/services/administrator.service.ts
  20. 84 0
      packages/core/src/service/services/history.service.ts
  21. 24 3
      packages/core/src/service/services/order.service.ts
  22. 18 7
      packages/core/src/service/services/payment-method.service.ts
  23. 1 1
      packages/dev-server/dev-config.ts
  24. 0 0
      schema-admin.json
  25. 0 0
      schema-shop.json

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

@@ -760,6 +760,50 @@ export type GlobalSettings = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type HistoryEntry = Node & {
+    __typename?: 'HistoryEntry';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    type: HistoryEntryType;
+    administrator?: Maybe<Administrator>;
+    data: Scalars['JSON'];
+};
+
+export type HistoryEntryFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    type?: Maybe<StringOperators>;
+};
+
+export type HistoryEntryList = PaginatedList & {
+    __typename?: 'HistoryEntryList';
+    items: Array<HistoryEntry>;
+    totalItems: Scalars['Int'];
+};
+
+export type HistoryEntryListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<HistoryEntrySortParameter>;
+    filter?: Maybe<HistoryEntryFilterParameter>;
+};
+
+export type HistoryEntrySortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+};
+
+export enum HistoryEntryType {
+    ORDER_STATE_TRANSITION = 'ORDER_STATE_TRANSITION',
+    ORDER_PAYMENT_TRANSITION = 'ORDER_PAYMENT_TRANSITION',
+    ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
+    ORDER_CANCELLATION = 'ORDER_CANCELLATION',
+    ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
+    ORDER_NOTE = 'ORDER_NOTE',
+}
+
 export type ImportInfo = {
     __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
@@ -1332,6 +1376,11 @@ export type Order = Node & {
     shippingMethod?: Maybe<ShippingMethod>;
     totalBeforeTax: Scalars['Int'];
     total: Scalars['Int'];
+    history: HistoryEntryList;
+};
+
+export type OrderHistoryArgs = {
+    options?: Maybe<HistoryEntryListOptions>;
 };
 
 export type OrderAddress = {
@@ -1748,6 +1797,7 @@ export type Refund = Node & {
     method?: Maybe<Scalars['String']>;
     state: Scalars['String'];
     transactionId?: Maybe<Scalars['String']>;
+    reason?: Maybe<Scalars['String']>;
     orderItems: Array<OrderItem>;
     paymentId: Scalars['ID'];
     metadata?: Maybe<Scalars['JSON']>;

+ 82 - 31
packages/common/src/generated-types.ts

@@ -1065,6 +1065,50 @@ export type GlobalSettings = {
   customFields?: Maybe<Scalars['JSON']>,
 };
 
+export type HistoryEntry = Node & {
+  __typename?: 'HistoryEntry',
+  id: Scalars['ID'],
+  createdAt: Scalars['DateTime'],
+  updatedAt: Scalars['DateTime'],
+  type: HistoryEntryType,
+  administrator?: Maybe<Administrator>,
+  data: Scalars['JSON'],
+};
+
+export type HistoryEntryFilterParameter = {
+  createdAt?: Maybe<DateOperators>,
+  updatedAt?: Maybe<DateOperators>,
+  type?: Maybe<StringOperators>,
+};
+
+export type HistoryEntryList = PaginatedList & {
+  __typename?: 'HistoryEntryList',
+  items: Array<HistoryEntry>,
+  totalItems: Scalars['Int'],
+};
+
+export type HistoryEntryListOptions = {
+  skip?: Maybe<Scalars['Int']>,
+  take?: Maybe<Scalars['Int']>,
+  sort?: Maybe<HistoryEntrySortParameter>,
+  filter?: Maybe<HistoryEntryFilterParameter>,
+};
+
+export type HistoryEntrySortParameter = {
+  id?: Maybe<SortOrder>,
+  createdAt?: Maybe<SortOrder>,
+  updatedAt?: Maybe<SortOrder>,
+};
+
+export enum HistoryEntryType {
+  ORDER_STATE_TRANSITION = 'ORDER_STATE_TRANSITION',
+  ORDER_PAYMENT_TRANSITION = 'ORDER_PAYMENT_TRANSITION',
+  ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
+  ORDER_CANCELLATION = 'ORDER_CANCELLATION',
+  ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
+  ORDER_NOTE = 'ORDER_NOTE'
+}
+
 export type ImportInfo = {
   __typename?: 'ImportInfo',
   errors?: Maybe<Array<Scalars['String']>>,
@@ -1540,8 +1584,8 @@ export type Mutation = {
   updateFacetValues: Array<FacetValue>,
   /** Delete one or more FacetValues */
   deleteFacetValues: Array<DeletionResponse>,
-  importProducts?: Maybe<ImportInfo>,
   updateGlobalSettings: GlobalSettings,
+  importProducts?: Maybe<ImportInfo>,
   settlePayment: Payment,
   fulfillOrder: Fulfillment,
   cancelOrder: Order,
@@ -1553,6 +1597,7 @@ export type Mutation = {
   createProductOptionGroup: ProductOptionGroup,
   /** Update an existing ProductOptionGroup */
   updateProductOptionGroup: ProductOptionGroup,
+  reindex: JobInfo,
   /** Create a new Product */
   createProduct: Product,
   /** Update an existing Product */
@@ -1567,7 +1612,6 @@ export type Mutation = {
   generateVariantsForProduct: Product,
   /** Update existing ProductVariants */
   updateProductVariants: Array<Maybe<ProductVariant>>,
-  reindex: JobInfo,
   createPromotion: Promotion,
   updatePromotion: Promotion,
   deletePromotion: DeletionResponse,
@@ -1579,14 +1623,14 @@ export type Mutation = {
   createShippingMethod: ShippingMethod,
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod,
-  /** Create a new TaxCategory */
-  createTaxCategory: TaxCategory,
-  /** Update an existing TaxCategory */
-  updateTaxCategory: TaxCategory,
   /** Create a new TaxRate */
   createTaxRate: TaxRate,
   /** Update an existing TaxRate */
   updateTaxRate: TaxRate,
+  /** Create a new TaxCategory */
+  createTaxCategory: TaxCategory,
+  /** Update an existing TaxCategory */
+  updateTaxCategory: TaxCategory,
   /** Create a new Zone */
   createZone: Zone,
   /** Update an existing Zone */
@@ -1754,13 +1798,13 @@ export type MutationDeleteFacetValuesArgs = {
 };
 
 
-export type MutationImportProductsArgs = {
-  csvFile: Scalars['Upload']
+export type MutationUpdateGlobalSettingsArgs = {
+  input: UpdateGlobalSettingsInput
 };
 
 
-export type MutationUpdateGlobalSettingsArgs = {
-  input: UpdateGlobalSettingsInput
+export type MutationImportProductsArgs = {
+  csvFile: Scalars['Upload']
 };
 
 
@@ -1879,23 +1923,23 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
-export type MutationCreateTaxCategoryArgs = {
-  input: CreateTaxCategoryInput
+export type MutationCreateTaxRateArgs = {
+  input: CreateTaxRateInput
 };
 
 
-export type MutationUpdateTaxCategoryArgs = {
-  input: UpdateTaxCategoryInput
+export type MutationUpdateTaxRateArgs = {
+  input: UpdateTaxRateInput
 };
 
 
-export type MutationCreateTaxRateArgs = {
-  input: CreateTaxRateInput
+export type MutationCreateTaxCategoryArgs = {
+  input: CreateTaxCategoryInput
 };
 
 
-export type MutationUpdateTaxRateArgs = {
-  input: UpdateTaxRateInput
+export type MutationUpdateTaxCategoryArgs = {
+  input: UpdateTaxCategoryInput
 };
 
 
@@ -1967,6 +2011,12 @@ export type Order = Node & {
   shippingMethod?: Maybe<ShippingMethod>,
   totalBeforeTax: Scalars['Int'],
   total: Scalars['Int'],
+  history: HistoryEntryList,
+};
+
+
+export type OrderHistoryArgs = {
+  options?: Maybe<HistoryEntryListOptions>
 };
 
 export type OrderAddress = {
@@ -2442,10 +2492,10 @@ export type Query = {
   paymentMethod?: Maybe<PaymentMethod>,
   productOptionGroups: Array<ProductOptionGroup>,
   productOptionGroup?: Maybe<ProductOptionGroup>,
+  search: SearchResponse,
   products: ProductList,
   /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
   product?: Maybe<Product>,
-  search: SearchResponse,
   promotion?: Maybe<Promotion>,
   promotions: PromotionList,
   adjustmentOperations: AdjustmentOperations,
@@ -2455,10 +2505,10 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
-  taxCategories: Array<TaxCategory>,
-  taxCategory?: Maybe<TaxCategory>,
   taxRates: TaxRateList,
   taxRate?: Maybe<TaxRate>,
+  taxCategories: Array<TaxCategory>,
+  taxCategory?: Maybe<TaxCategory>,
   zones: Array<Zone>,
   zone?: Maybe<Zone>,
 };
@@ -2580,6 +2630,11 @@ export type QueryProductOptionGroupArgs = {
 };
 
 
+export type QuerySearchArgs = {
+  input: SearchInput
+};
+
+
 export type QueryProductsArgs = {
   languageCode?: Maybe<LanguageCode>,
   options?: Maybe<ProductListOptions>
@@ -2593,11 +2648,6 @@ export type QueryProductArgs = {
 };
 
 
-export type QuerySearchArgs = {
-  input: SearchInput
-};
-
-
 export type QueryPromotionArgs = {
   id: Scalars['ID']
 };
@@ -2628,11 +2678,6 @@ export type QueryShippingMethodArgs = {
 };
 
 
-export type QueryTaxCategoryArgs = {
-  id: Scalars['ID']
-};
-
-
 export type QueryTaxRatesArgs = {
   options?: Maybe<TaxRateListOptions>
 };
@@ -2643,6 +2688,11 @@ export type QueryTaxRateArgs = {
 };
 
 
+export type QueryTaxCategoryArgs = {
+  id: Scalars['ID']
+};
+
+
 export type QueryZoneArgs = {
   id: Scalars['ID']
 };
@@ -2659,6 +2709,7 @@ export type Refund = Node & {
   method?: Maybe<Scalars['String']>,
   state: Scalars['String'],
   transactionId?: Maybe<Scalars['String']>,
+  reason?: Maybe<Scalars['String']>,
   orderItems: Array<OrderItem>,
   paymentId: Scalars['ID'],
   metadata?: Maybe<Scalars['JSON']>,

+ 111 - 29
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1063,6 +1063,50 @@ export type GlobalSettings = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type HistoryEntry = Node & {
+    __typename?: 'HistoryEntry';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    type: HistoryEntryType;
+    administrator?: Maybe<Administrator>;
+    data: Scalars['JSON'];
+};
+
+export type HistoryEntryFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    type?: Maybe<StringOperators>;
+};
+
+export type HistoryEntryList = PaginatedList & {
+    __typename?: 'HistoryEntryList';
+    items: Array<HistoryEntry>;
+    totalItems: Scalars['Int'];
+};
+
+export type HistoryEntryListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<HistoryEntrySortParameter>;
+    filter?: Maybe<HistoryEntryFilterParameter>;
+};
+
+export type HistoryEntrySortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+};
+
+export enum HistoryEntryType {
+    ORDER_STATE_TRANSITION = 'ORDER_STATE_TRANSITION',
+    ORDER_PAYMENT_TRANSITION = 'ORDER_PAYMENT_TRANSITION',
+    ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
+    ORDER_CANCELLATION = 'ORDER_CANCELLATION',
+    ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
+    ORDER_NOTE = 'ORDER_NOTE',
+}
+
 export type ImportInfo = {
     __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
@@ -1537,8 +1581,8 @@ export type Mutation = {
     updateFacetValues: Array<FacetValue>;
     /** Delete one or more FacetValues */
     deleteFacetValues: Array<DeletionResponse>;
-    importProducts?: Maybe<ImportInfo>;
     updateGlobalSettings: GlobalSettings;
+    importProducts?: Maybe<ImportInfo>;
     settlePayment: Payment;
     fulfillOrder: Fulfillment;
     cancelOrder: Order;
@@ -1550,6 +1594,7 @@ export type Mutation = {
     createProductOptionGroup: ProductOptionGroup;
     /** Update an existing ProductOptionGroup */
     updateProductOptionGroup: ProductOptionGroup;
+    reindex: JobInfo;
     /** Create a new Product */
     createProduct: Product;
     /** Update an existing Product */
@@ -1564,7 +1609,6 @@ export type Mutation = {
     generateVariantsForProduct: Product;
     /** Update existing ProductVariants */
     updateProductVariants: Array<Maybe<ProductVariant>>;
-    reindex: JobInfo;
     createPromotion: Promotion;
     updatePromotion: Promotion;
     deletePromotion: DeletionResponse;
@@ -1576,14 +1620,14 @@ export type Mutation = {
     createShippingMethod: ShippingMethod;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
-    /** Create a new TaxCategory */
-    createTaxCategory: TaxCategory;
-    /** Update an existing TaxCategory */
-    updateTaxCategory: TaxCategory;
     /** Create a new TaxRate */
     createTaxRate: TaxRate;
     /** Update an existing TaxRate */
     updateTaxRate: TaxRate;
+    /** Create a new TaxCategory */
+    createTaxCategory: TaxCategory;
+    /** Update an existing TaxCategory */
+    updateTaxCategory: TaxCategory;
     /** Create a new Zone */
     createZone: Zone;
     /** Update an existing Zone */
@@ -1721,14 +1765,14 @@ export type MutationDeleteFacetValuesArgs = {
     force?: Maybe<Scalars['Boolean']>;
 };
 
-export type MutationImportProductsArgs = {
-    csvFile: Scalars['Upload'];
-};
-
 export type MutationUpdateGlobalSettingsArgs = {
     input: UpdateGlobalSettingsInput;
 };
 
+export type MutationImportProductsArgs = {
+    csvFile: Scalars['Upload'];
+};
+
 export type MutationSettlePaymentArgs = {
     id: Scalars['ID'];
 };
@@ -1822,14 +1866,6 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
-export type MutationCreateTaxCategoryArgs = {
-    input: CreateTaxCategoryInput;
-};
-
-export type MutationUpdateTaxCategoryArgs = {
-    input: UpdateTaxCategoryInput;
-};
-
 export type MutationCreateTaxRateArgs = {
     input: CreateTaxRateInput;
 };
@@ -1838,6 +1874,14 @@ export type MutationUpdateTaxRateArgs = {
     input: UpdateTaxRateInput;
 };
 
+export type MutationCreateTaxCategoryArgs = {
+    input: CreateTaxCategoryInput;
+};
+
+export type MutationUpdateTaxCategoryArgs = {
+    input: UpdateTaxCategoryInput;
+};
+
 export type MutationCreateZoneArgs = {
     input: CreateZoneInput;
 };
@@ -1902,6 +1946,11 @@ export type Order = Node & {
     shippingMethod?: Maybe<ShippingMethod>;
     totalBeforeTax: Scalars['Int'];
     total: Scalars['Int'];
+    history: HistoryEntryList;
+};
+
+export type OrderHistoryArgs = {
+    options?: Maybe<HistoryEntryListOptions>;
 };
 
 export type OrderAddress = {
@@ -2376,10 +2425,10 @@ export type Query = {
     paymentMethod?: Maybe<PaymentMethod>;
     productOptionGroups: Array<ProductOptionGroup>;
     productOptionGroup?: Maybe<ProductOptionGroup>;
+    search: SearchResponse;
     products: ProductList;
     /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
     product?: Maybe<Product>;
-    search: SearchResponse;
     promotion?: Maybe<Promotion>;
     promotions: PromotionList;
     adjustmentOperations: AdjustmentOperations;
@@ -2389,10 +2438,10 @@ export type Query = {
     shippingMethod?: Maybe<ShippingMethod>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
-    taxCategories: Array<TaxCategory>;
-    taxCategory?: Maybe<TaxCategory>;
     taxRates: TaxRateList;
     taxRate?: Maybe<TaxRate>;
+    taxCategories: Array<TaxCategory>;
+    taxCategory?: Maybe<TaxCategory>;
     zones: Array<Zone>;
     zone?: Maybe<Zone>;
 };
@@ -2491,6 +2540,10 @@ export type QueryProductOptionGroupArgs = {
     languageCode?: Maybe<LanguageCode>;
 };
 
+export type QuerySearchArgs = {
+    input: SearchInput;
+};
+
 export type QueryProductsArgs = {
     languageCode?: Maybe<LanguageCode>;
     options?: Maybe<ProductListOptions>;
@@ -2502,10 +2555,6 @@ export type QueryProductArgs = {
     languageCode?: Maybe<LanguageCode>;
 };
 
-export type QuerySearchArgs = {
-    input: SearchInput;
-};
-
 export type QueryPromotionArgs = {
     id: Scalars['ID'];
 };
@@ -2530,10 +2579,6 @@ export type QueryShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
-export type QueryTaxCategoryArgs = {
-    id: Scalars['ID'];
-};
-
 export type QueryTaxRatesArgs = {
     options?: Maybe<TaxRateListOptions>;
 };
@@ -2542,6 +2587,10 @@ export type QueryTaxRateArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryTaxCategoryArgs = {
+    id: Scalars['ID'];
+};
+
 export type QueryZoneArgs = {
     id: Scalars['ID'];
 };
@@ -2558,6 +2607,7 @@ export type Refund = Node & {
     method?: Maybe<Scalars['String']>;
     state: Scalars['String'];
     transactionId?: Maybe<Scalars['String']>;
+    reason?: Maybe<Scalars['String']>;
     orderItems: Array<OrderItem>;
     paymentId: Scalars['ID'];
     metadata?: Maybe<Scalars['JSON']>;
@@ -4096,6 +4146,27 @@ export type SettleRefundMutation = { __typename?: 'Mutation' } & {
     >;
 };
 
+export type GetOrderHistoryQueryVariables = {
+    id: Scalars['ID'];
+    options?: Maybe<HistoryEntryListOptions>;
+};
+
+export type GetOrderHistoryQuery = { __typename?: 'Query' } & {
+    order: Maybe<
+        { __typename?: 'Order' } & Pick<Order, 'id'> & {
+                history: { __typename?: 'HistoryEntryList' } & Pick<HistoryEntryList, 'totalItems'> & {
+                        items: Array<
+                            { __typename?: 'HistoryEntry' } & Pick<HistoryEntry, 'id' | 'type' | 'data'> & {
+                                    administrator: Maybe<
+                                        { __typename?: 'Administrator' } & Pick<Administrator, 'id'>
+                                    >;
+                                }
+                        >;
+                    };
+            }
+    >;
+};
+
 export type AddOptionGroupToProductMutationVariables = {
     productId: Scalars['ID'];
     optionGroupId: Scalars['ID'];
@@ -5085,6 +5156,17 @@ export namespace SettleRefund {
     export type SettleRefund = SettleRefundMutation['settleRefund'];
 }
 
+export namespace GetOrderHistory {
+    export type Variables = GetOrderHistoryQueryVariables;
+    export type Query = GetOrderHistoryQuery;
+    export type Order = NonNullable<GetOrderHistoryQuery['order']>;
+    export type History = (NonNullable<GetOrderHistoryQuery['order']>)['history'];
+    export type Items = NonNullable<(NonNullable<GetOrderHistoryQuery['order']>)['history']['items'][0]>;
+    export type Administrator = NonNullable<
+        (NonNullable<(NonNullable<GetOrderHistoryQuery['order']>)['history']['items'][0]>)['administrator']
+    >;
+}
+
 export namespace AddOptionGroupToProduct {
     export type Variables = AddOptionGroupToProductMutationVariables;
     export type Mutation = AddOptionGroupToProductMutation;

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

@@ -760,6 +760,50 @@ export type GlobalSettings = {
     customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type HistoryEntry = Node & {
+    __typename?: 'HistoryEntry';
+    id: Scalars['ID'];
+    createdAt: Scalars['DateTime'];
+    updatedAt: Scalars['DateTime'];
+    type: HistoryEntryType;
+    administrator?: Maybe<Administrator>;
+    data: Scalars['JSON'];
+};
+
+export type HistoryEntryFilterParameter = {
+    createdAt?: Maybe<DateOperators>;
+    updatedAt?: Maybe<DateOperators>;
+    type?: Maybe<StringOperators>;
+};
+
+export type HistoryEntryList = PaginatedList & {
+    __typename?: 'HistoryEntryList';
+    items: Array<HistoryEntry>;
+    totalItems: Scalars['Int'];
+};
+
+export type HistoryEntryListOptions = {
+    skip?: Maybe<Scalars['Int']>;
+    take?: Maybe<Scalars['Int']>;
+    sort?: Maybe<HistoryEntrySortParameter>;
+    filter?: Maybe<HistoryEntryFilterParameter>;
+};
+
+export type HistoryEntrySortParameter = {
+    id?: Maybe<SortOrder>;
+    createdAt?: Maybe<SortOrder>;
+    updatedAt?: Maybe<SortOrder>;
+};
+
+export enum HistoryEntryType {
+    ORDER_STATE_TRANSITION = 'ORDER_STATE_TRANSITION',
+    ORDER_PAYMENT_TRANSITION = 'ORDER_PAYMENT_TRANSITION',
+    ORDER_FULLFILLMENT = 'ORDER_FULLFILLMENT',
+    ORDER_CANCELLATION = 'ORDER_CANCELLATION',
+    ORDER_REFUND_TRANSITION = 'ORDER_REFUND_TRANSITION',
+    ORDER_NOTE = 'ORDER_NOTE',
+}
+
 export type ImportInfo = {
     __typename?: 'ImportInfo';
     errors?: Maybe<Array<Scalars['String']>>;
@@ -1332,6 +1376,11 @@ export type Order = Node & {
     shippingMethod?: Maybe<ShippingMethod>;
     totalBeforeTax: Scalars['Int'];
     total: Scalars['Int'];
+    history: HistoryEntryList;
+};
+
+export type OrderHistoryArgs = {
+    options?: Maybe<HistoryEntryListOptions>;
 };
 
 export type OrderAddress = {
@@ -1748,6 +1797,7 @@ export type Refund = Node & {
     method?: Maybe<Scalars['String']>;
     state: Scalars['String'];
     transactionId?: Maybe<Scalars['String']>;
+    reason?: Maybe<Scalars['String']>;
     orderItems: Array<OrderItem>;
     paymentId: Scalars['ID'];
     metadata?: Maybe<Scalars['JSON']>;

+ 216 - 5
packages/core/e2e/order.e2e-spec.ts

@@ -2,12 +2,10 @@
 import gql from 'graphql-tag';
 import path from 'path';
 
-import { StockMovementType } from '../../common/lib/generated-types';
+import { HistoryEntryType, StockMovementType } from '../../common/lib/generated-types';
 import { pick } from '../../common/lib/pick';
 import { ID } from '../../common/lib/shared-types';
 import { PaymentMethodHandler } from '../src/config/payment-method/payment-method-handler';
-import { PaymentMetadata } from '../src/entity/payment/payment.entity';
-import { RefundState } from '../src/service/helpers/refund-state-machine/refund-state';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { ORDER_FRAGMENT, ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
@@ -18,12 +16,15 @@ import {
     GetOrder,
     GetOrderFulfillmentItems,
     GetOrderFulfillments,
+    GetOrderHistory,
     GetOrderList,
     GetOrderListFulfillments,
     GetProductWithVariants,
     GetStockMovement,
-    OrderItemFragment, RefundOrder,
-    SettlePayment, SettleRefund,
+    OrderItemFragment,
+    RefundOrder,
+    SettlePayment,
+    SettleRefund,
     UpdateProductVariants,
 } from './graphql/generated-e2e-admin-types';
 import {
@@ -116,6 +117,12 @@ describe('Orders resolver', () => {
         expect(result.order!.id).toBe('T_2');
     });
 
+    it('order history initially empty', async () => {
+        const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, { id: 'T_1' });
+        expect(order!.history.totalItems).toBe(0);
+        expect(order!.history.items).toEqual([]);
+    });
+
     describe('payments', () => {
         it('settlePayment fails', async () => {
             await shopClient.asUserWithCredentials(customers[0].emailAddress, password);
@@ -196,6 +203,44 @@ describe('Orders resolver', () => {
             expect(result.order!.state).toBe('PaymentSettled');
             expect(result.order!.payments![0].state).toBe('Settled');
         });
+
+        it('order history contains expected entries', async () => {
+            const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, { id: 'T_2' });
+            expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'AddingItems',
+                        to: 'ArrangingPayment',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
+                        paymentId: 'T_2',
+                        from: 'Created',
+                        to: 'Authorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'ArrangingPayment',
+                        to: 'PaymentAuthorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
+                        paymentId: 'T_2',
+                        from: 'Authorized',
+                        to: 'Settled',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PaymentAuthorized',
+                        to: 'PaymentSettled',
+                    },
+                },
+            ]);
+        });
     });
 
     describe('fulfillment', () => {
@@ -382,6 +427,51 @@ describe('Orders resolver', () => {
             expect(result.order!.state).toBe('Fulfilled');
         });
 
+        it('order history contains expected entries', async () => {
+            const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
+                id: 'T_2',
+                options: {
+                    skip: 5,
+                },
+            });
+            expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
+                        fulfillmentId: 'T_1',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PaymentSettled',
+                        to: 'PartiallyFulfilled',
+                    },
+                },
+
+                {
+                    type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
+                        fulfillmentId: 'T_2',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PartiallyFulfilled',
+                        to: 'PartiallyFulfilled',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_FULLFILLMENT, data: {
+                        fulfillmentId: 'T_3',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PartiallyFulfilled',
+                        to: 'Fulfilled',
+                    },
+                },
+            ]);
+        });
+
         it('order.fullfillments resolver for single order', async () => {
             const { order } = await adminClient.query<
                 GetOrderFulfillments.Query,
@@ -556,6 +646,7 @@ describe('Orders resolver', () => {
             const { cancelOrder } = await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
                 input: {
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
+                    reason: 'cancel reason 1',
                 },
             });
 
@@ -594,6 +685,7 @@ describe('Orders resolver', () => {
             await adminClient.query<CancelOrder.Mutation, CancelOrder.Variables>(CANCEL_ORDER, {
                 input: {
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: 1 })),
+                    reason: 'cancel reason 2',
                 },
             });
 
@@ -617,6 +709,54 @@ describe('Orders resolver', () => {
                 { type: StockMovementType.CANCELLATION, quantity: 1 },
             ]);
         });
+
+        it('order history contains expected entries', async () => {
+            const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
+                id: orderId,
+                options: {
+                    skip: 0,
+                },
+            });
+            expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'AddingItems',
+                        to: 'ArrangingPayment',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
+                        paymentId: 'T_3',
+                        from: 'Created',
+                        to: 'Authorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'ArrangingPayment',
+                        to: 'PaymentAuthorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_CANCELLATION, data: {
+                        orderItemIds: ['T_7'],
+                        reason: 'cancel reason 1',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_CANCELLATION, data: {
+                        orderItemIds: ['T_8'],
+                        reason: 'cancel reason 2',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PaymentAuthorized',
+                        to: 'Cancelled',
+                    },
+                },
+            ]);
+        });
     });
 
     describe('refunds', () => {
@@ -762,6 +902,7 @@ describe('Orders resolver', () => {
                     lines: order!.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
                     shipping: order!.shipping,
                     adjustment: 0,
+                    reason: 'foo',
                     paymentId,
                 },
             });
@@ -802,6 +943,57 @@ describe('Orders resolver', () => {
             expect(settleRefund.state).toBe('Settled');
             expect(settleRefund.transactionId).toBe('aaabbb');
         });
+
+        it('order history contains expected entries', async () => {
+            const { order } = await adminClient.query<GetOrderHistory.Query, GetOrderHistory.Variables>(GET_ORDER_HISTORY, {
+                id: orderId,
+                options: {
+                    skip: 0,
+                },
+            });
+            expect(order!.history.items.map(pick(['type', 'data']))).toEqual([
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'AddingItems',
+                        to: 'ArrangingPayment',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
+                        paymentId: 'T_4',
+                        from: 'Created',
+                        to: 'Authorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'ArrangingPayment',
+                        to: 'PaymentAuthorized',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_PAYMENT_TRANSITION, data: {
+                        paymentId: 'T_4',
+                        from: 'Authorized',
+                        to: 'Settled',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_STATE_TRANSITION, data: {
+                        from: 'PaymentAuthorized',
+                        to: 'PaymentSettled',
+                    },
+                },
+                {
+                    type: HistoryEntryType.ORDER_REFUND_TRANSITION, data: {
+                        refundId: 'T_1',
+                        reason: 'foo',
+                        from: 'Pending',
+                        to: 'Settled',
+                    },
+                },
+            ]);
+        });
     });
 });
 
@@ -1033,3 +1225,22 @@ export const SETTLE_REFUND = gql`
         }
     }
 `;
+
+export const GET_ORDER_HISTORY = gql`
+    query GetOrderHistory($id: ID!, $options: HistoryEntryListOptions) {
+        order(id: $id) {
+            id
+            history(options: $options) {
+                totalItems
+                items {
+                    id
+                    type
+                    administrator {
+                        id
+                    }
+                    data
+                }
+            }
+        }
+    }
+`;

+ 15 - 2
packages/core/src/api/common/id-codec.ts

@@ -3,7 +3,16 @@ import { ID } from '@vendure/common/lib/shared-types';
 import { EntityIdStrategy } from '../../config/entity-id-strategy/entity-id-strategy';
 import { VendureEntity } from '../../entity/base/base.entity';
 
-const ID_KEYS = ['id', 'productId', 'productVariantId', 'collectionIds'];
+const ID_KEYS = [
+    'id',
+    'productId',
+    'productVariantId',
+    'collectionIds',
+    'paymentId',
+    'fulfillmentId',
+    'orderItemIds',
+    'refundId',
+];
 
 /**
  * This service is responsible for encoding/decoding entity IDs according to the configured EntityIdStrategy.
@@ -85,7 +94,11 @@ export class IdCodec {
             target = this.transform(target, transformFn, transformKeys);
             for (const key of Object.keys(target)) {
                 if (this.isObject(target[key as keyof T])) {
-                    target[key as keyof T] = this.transformRecursive(target[key as keyof T], transformFn, transformKeys);
+                    target[key as keyof T] = this.transformRecursive(
+                        target[key as keyof T],
+                        transformFn,
+                        transformKeys,
+                    );
                 }
             }
         }

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

@@ -1,12 +1,16 @@
-import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { Args, Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { OrderHistoryArgs } from '@vendure/common/lib/generated-types';
 
 import { Order } from '../../../entity/order/order.entity';
+import { HistoryService } from '../../../service/services/history.service';
 import { OrderService } from '../../../service/services/order.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
 
 @Resolver('Order')
 export class OrderEntityResolver {
-    constructor(private orderService: OrderService, private shippingMethodService: ShippingMethodService) {}
+    constructor(private orderService: OrderService,
+                private shippingMethodService: ShippingMethodService,
+                private historyService: HistoryService) {}
 
     @ResolveProperty()
     async payments(@Parent() order: Order) {
@@ -32,4 +36,9 @@ export class OrderEntityResolver {
     async fulfillments(@Parent() order: Order) {
         return this.orderService.getOrderFulfillments(order);
     }
+
+    @ResolveProperty()
+    async history(@Parent() order: Order, @Args() args: OrderHistoryArgs) {
+        return this.historyService.getHistoryForOrder(order.id, args.options || undefined);
+    }
 }

+ 25 - 0
packages/core/src/api/schema/type/history-entry.type.graphql

@@ -0,0 +1,25 @@
+type HistoryEntry implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    type: HistoryEntryType!
+    administrator: Administrator
+    data: JSON!
+}
+
+enum HistoryEntryType {
+    ORDER_STATE_TRANSITION
+    ORDER_PAYMENT_TRANSITION
+    ORDER_FULLFILLMENT
+    ORDER_CANCELLATION
+    ORDER_REFUND_TRANSITION
+    ORDER_NOTE
+}
+
+type HistoryEntryList implements PaginatedList {
+    items: [HistoryEntry!]!
+    totalItems: Int!
+}
+
+# generated by generateListOptions function
+input HistoryEntryListOptions

+ 2 - 0
packages/core/src/api/schema/type/order.type.graphql

@@ -20,6 +20,7 @@ type Order implements Node {
     shippingMethod: ShippingMethod
     totalBeforeTax: Int!
     total: Int!
+    history(options: HistoryEntryListOptions): HistoryEntryList!
 }
 
 type OrderAddress {
@@ -99,6 +100,7 @@ type Refund implements Node {
     method: String
     state: String!
     transactionId: String
+    reason: String
     orderItems: [OrderItem!]!
     paymentId: ID!
     metadata: JSON

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

@@ -14,6 +14,8 @@ import { FacetTranslation } from './facet/facet-translation.entity';
 import { Facet } from './facet/facet.entity';
 import { Fulfillment } from './fulfillment/fulfillment.entity';
 import { GlobalSettings } from './global-settings/global-settings.entity';
+import { HistoryEntry } from './history-entry/history-entry.entity';
+import { OrderHistoryEntry } from './history-entry/order-history-entry.entity';
 import { OrderItem } from './order-item/order-item.entity';
 import { OrderLine } from './order-line/order-line.entity';
 import { Order } from './order/order.entity';
@@ -67,7 +69,9 @@ export const coreEntitiesMap = {
     FacetValueTranslation,
     Fulfillment,
     GlobalSettings,
+    HistoryEntry,
     Order,
+    OrderHistoryEntry,
     OrderItem,
     OrderLine,
     Payment,

+ 21 - 0
packages/core/src/entity/history-entry/history-entry.entity.ts

@@ -0,0 +1,21 @@
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
+import { Column, Entity, ManyToOne, TableInheritance } from 'typeorm';
+
+import { Administrator } from '../administrator/administrator.entity';
+import { VendureEntity } from '../base/base.entity';
+
+@Entity()
+@TableInheritance({ column: { type: 'varchar', name: 'discriminator' } })
+export abstract class HistoryEntry extends VendureEntity {
+    @ManyToOne(type => Administrator)
+    administrator?: Administrator;
+
+    @Column({ nullable: false, type: 'varchar' })
+    readonly type: HistoryEntryType;
+
+    @Column()
+    isPublic: boolean;
+
+    @Column('simple-json')
+    data: any;
+}

+ 16 - 0
packages/core/src/entity/history-entry/order-history-entry.entity.ts

@@ -0,0 +1,16 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { Order } from '../order/order.entity';
+
+import { HistoryEntry } from './history-entry.entity';
+
+@ChildEntity()
+export class OrderHistoryEntry extends HistoryEntry {
+    constructor(input: DeepPartial<OrderHistoryEntry>) {
+        super(input);
+    }
+
+    @ManyToOne(type => Order)
+    order: Order;
+}

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

@@ -23,6 +23,8 @@ export class Refund extends VendureEntity {
 
     @Column() method: string;
 
+    @Column({ nullable: true }) reason: string;
+
     @Column('varchar') state: RefundState;
 
     @Column({ nullable: true }) transactionId: string;

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

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
@@ -7,6 +8,7 @@ import { ConfigService } from '../../../config/config.service';
 import { Order } from '../../../entity/order/order.entity';
 import { EventBus } from '../../../event-bus/event-bus';
 import { OrderStateTransitionEvent } from '../../../event-bus/events/order-state-transition-event';
+import { HistoryService } from '../../services/history.service';
 import { StockMovementService } from '../../services/stock-movement.service';
 
 import { OrderState, orderStateTransitions, OrderTransitionData } from './order-state';
@@ -18,6 +20,7 @@ export class OrderStateMachine {
 
     constructor(private configService: ConfigService,
                 private stockMovementService: StockMovementService,
+                private historyService: HistoryService,
                 private eventBus: EventBus) {
         this.config = this.initConfig();
     }
@@ -65,6 +68,15 @@ export class OrderStateMachine {
             await this.stockMovementService.createSalesForOrder(data.order);
         }
         this.eventBus.publish(new OrderStateTransitionEvent(fromState, toState, data.ctx, data.order));
+        await this.historyService.createHistoryEntryForOrder({
+            orderId: data.order.id,
+            type: HistoryEntryType.ORDER_STATE_TRANSITION,
+            ctx: data.ctx,
+            data: {
+                from: fromState,
+                to: toState,
+            },
+        });
     }
 
     private initConfig(): StateMachineConfig<OrderState, OrderTransitionData> {

+ 14 - 1
packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
@@ -8,6 +9,7 @@ import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { EventBus } from '../../../event-bus/event-bus';
 import { PaymentStateTransitionEvent } from '../../../event-bus/events/payment-state-transition-event';
+import { HistoryService } from '../../services/history.service';
 
 import { PaymentState, paymentStateTransitions, PaymentTransitionData } from './payment-state';
 
@@ -19,8 +21,18 @@ export class PaymentStateMachine {
         onTransitionStart: async (fromState, toState, data) => {
             return true;
         },
-        onTransitionEnd: (fromState, toState, data) => {
+        onTransitionEnd: async (fromState, toState, data) => {
             this.eventBus.publish(new PaymentStateTransitionEvent(fromState, toState, data.ctx, data.payment, data.order));
+            await this.historyService.createHistoryEntryForOrder({
+                ctx: data.ctx,
+                orderId: data.order.id,
+                type: HistoryEntryType.ORDER_PAYMENT_TRANSITION,
+                data: {
+                    paymentId: data.payment.id,
+                    from: fromState,
+                    to: toState,
+                },
+            });
         },
         onError: (fromState, toState, message) => {
             throw new IllegalOperationError(message || 'error.cannot-transition-payment-from-to', {
@@ -31,6 +43,7 @@ export class PaymentStateMachine {
     };
 
     constructor(private configService: ConfigService,
+                private historyService: HistoryService,
                 private eventBus: EventBus) {}
 
     getNextStates(payment: Payment): PaymentState[] {

+ 4 - 1
packages/core/src/service/helpers/payment-state-machine/payment-state.ts

@@ -9,9 +9,12 @@ import { Payment } from '../../../entity/payment/payment.entity';
  *
  * @docsCategory payment
  */
-export type PaymentState = 'Authorized' | 'Settled' | 'Declined';
+export type PaymentState = 'Created' | 'Authorized' | 'Settled' | 'Declined';
 
 export const paymentStateTransitions: Transitions<PaymentState> = {
+    Created: {
+        to: ['Authorized', 'Settled', 'Declined'],
+    },
     Authorized: {
         to: ['Settled'],
     },

+ 15 - 1
packages/core/src/service/helpers/refund-state-machine/refund-state-machine.ts

@@ -1,4 +1,5 @@
 import { Injectable } from '@nestjs/common';
+import { HistoryEntryType } from '@vendure/common/lib/generated-types';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { IllegalOperationError } from '../../../common/error/errors';
@@ -8,6 +9,7 @@ import { Order } from '../../../entity/order/order.entity';
 import { Refund } from '../../../entity/refund/refund.entity';
 import { EventBus } from '../../../event-bus/event-bus';
 import { RefundStateTransitionEvent } from '../../../event-bus/events/refund-state-transition-event';
+import { HistoryService } from '../../services/history.service';
 
 import { RefundState, refundStateTransitions, RefundTransitionData } from './refund-state';
 
@@ -19,8 +21,19 @@ export class RefundStateMachine {
         onTransitionStart: async (fromState, toState, data) => {
             return true;
         },
-        onTransitionEnd: (fromState, toState, data) => {
+        onTransitionEnd: async (fromState, toState, data) => {
             this.eventBus.publish(new RefundStateTransitionEvent(fromState, toState, data.ctx, data.refund, data.order));
+            await this.historyService.createHistoryEntryForOrder({
+                ctx: data.ctx,
+                orderId: data.order.id,
+                type: HistoryEntryType.ORDER_REFUND_TRANSITION,
+                data: {
+                    refundId: data.refund.id,
+                    from: fromState,
+                    to: toState,
+                    reason: data.refund.reason,
+                },
+            });
         },
         onError: (fromState, toState, message) => {
             throw new IllegalOperationError(message || 'error.cannot-transition-refund-from-to', {
@@ -31,6 +44,7 @@ export class RefundStateMachine {
     };
 
     constructor(private configService: ConfigService,
+                private historyService: HistoryService,
                 private eventBus: EventBus) {}
 
     getNextStates(refund: Refund): RefundState[] {

+ 6 - 4
packages/core/src/service/service.module.ts

@@ -28,6 +28,7 @@ import { CustomerService } from './services/customer.service';
 import { FacetValueService } from './services/facet-value.service';
 import { FacetService } from './services/facet.service';
 import { GlobalSettingsService } from './services/global-settings.service';
+import { HistoryService } from './services/history.service';
 import { JobService } from './services/job.service';
 import { OrderService } from './services/order.service';
 import { PaymentMethodService } from './services/payment-method.service';
@@ -46,28 +47,29 @@ import { UserService } from './services/user.service';
 import { ZoneService } from './services/zone.service';
 
 const exportedProviders = [
-    PromotionService,
     AdministratorService,
     AssetService,
     AuthService,
     ChannelService,
+    CollectionService,
     CountryService,
     CustomerGroupService,
     CustomerService,
     FacetService,
     FacetValueService,
     GlobalSettingsService,
+    HistoryService,
     JobService,
     OrderService,
     PaymentMethodService,
-    CollectionService,
-    ProductOptionService,
     ProductOptionGroupService,
+    ProductOptionService,
     ProductService,
     ProductVariantService,
+    PromotionService,
     RoleService,
-    ShippingMethodService,
     SearchService,
+    ShippingMethodService,
     StockMovementService,
     TaxCategoryService,
     TaxRateService,

+ 9 - 0
packages/core/src/service/services/administrator.service.ts

@@ -46,6 +46,15 @@ export class AdministratorService {
         });
     }
 
+    findOneByUserId(userId: ID): Promise<Administrator | undefined> {
+        return this.connection.getRepository(Administrator).findOne({
+            where: {
+                user: { id: userId },
+                deletedAt: null,
+            },
+        });
+    }
+
     async create(input: CreateAdministratorInput): Promise<Administrator> {
         const administrator = new Administrator(input);
         administrator.user = await this.userService.createAdminUser(input.emailAddress, input.password);

+ 84 - 0
packages/core/src/service/services/history.service.ts

@@ -0,0 +1,84 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { HistoryEntryListOptions, HistoryEntryType } from '@vendure/common/lib/generated-types';
+import { ID, PaginatedList, Type } from '@vendure/common/lib/shared-types';
+import { Connection } from 'typeorm';
+
+import { RequestContext } from '../../api/common/request-context';
+import { HistoryEntry } from '../../entity/history-entry/history-entry.entity';
+import { OrderHistoryEntry } from '../../entity/history-entry/order-history-entry.entity';
+import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { OrderState } from '../helpers/order-state-machine/order-state';
+import { PaymentState } from '../helpers/payment-state-machine/payment-state';
+import { RefundState } from '../helpers/refund-state-machine/refund-state';
+
+import { AdministratorService } from './administrator.service';
+
+export type OrderHistoryEntryData = {
+    [HistoryEntryType.ORDER_STATE_TRANSITION]: {
+        from: OrderState;
+        to: OrderState;
+    };
+    [HistoryEntryType.ORDER_PAYMENT_TRANSITION]: {
+        paymentId: ID;
+        from: PaymentState;
+        to: PaymentState;
+    };
+    [HistoryEntryType.ORDER_FULLFILLMENT]: {
+        fulfillmentId: ID;
+    };
+    [HistoryEntryType.ORDER_CANCELLATION]: {
+        orderItemIds: ID[];
+        reason?: string;
+    };
+    [HistoryEntryType.ORDER_REFUND_TRANSITION]: {
+        refundId: ID;
+        from: RefundState;
+        to: RefundState;
+        reason?: string;
+    };
+};
+
+export interface CreateOrderHistoryEntryArgs<T extends keyof OrderHistoryEntryData> {
+    orderId: ID;
+    ctx: RequestContext;
+    type: T;
+    data: OrderHistoryEntryData[T];
+}
+
+/**
+ * The HistoryService is reponsible for creating and retrieving HistoryEntry entities.
+ */
+@Injectable()
+export class HistoryService {
+    constructor(@InjectConnection() private connection: Connection,
+                private administratorService: AdministratorService,
+                private listQueryBuilder: ListQueryBuilder) {}
+
+    async getHistoryForOrder(orderId: ID, options?: HistoryEntryListOptions): Promise<PaginatedList<OrderHistoryEntry>> {
+        return this.listQueryBuilder.build(HistoryEntry as any as Type<OrderHistoryEntry>, options, {
+            where: {
+                order: { id: orderId } as any,
+            },
+            relations: ['administrator'],
+        }).getManyAndCount()
+            .then(([items, totalItems]) => ({
+                items,
+                totalItems,
+            }));
+    }
+
+    async createHistoryEntryForOrder<T extends keyof OrderHistoryEntryData>(args: CreateOrderHistoryEntryArgs<T>): Promise<OrderHistoryEntry> {
+        const {ctx, data, orderId, type} = args;
+        const administrator = ctx.activeUserId ? await this.administratorService.findOneByUserId(ctx.activeUserId) : undefined;
+        const entry = new OrderHistoryEntry({
+            type,
+            // TODO: figure out which should be public and not
+            isPublic: true,
+            data: data as any,
+            order: { id: orderId },
+            administrator,
+        });
+        return this.connection.getRepository(OrderHistoryEntry).save(entry);
+    }
+}

+ 24 - 3
packages/core/src/service/services/order.service.ts

@@ -4,6 +4,7 @@ import {
     CancelOrderInput,
     CreateAddressInput,
     FulfillOrderInput,
+    HistoryEntryType,
     OrderLineInput,
     RefundOrderInput,
     SettleRefundInput,
@@ -43,6 +44,7 @@ import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { CountryService } from './country.service';
 import { CustomerService } from './customer.service';
+import { HistoryService } from './history.service';
 import { PaymentMethodService } from './payment-method.service';
 import { ProductVariantService } from './product-variant.service';
 import { StockMovementService } from './stock-movement.service';
@@ -63,6 +65,7 @@ export class OrderService {
         private listQueryBuilder: ListQueryBuilder,
         private stockMovementService: StockMovementService,
         private refundStateMachine: RefundStateMachine,
+        private historyService: HistoryService,
     ) {}
 
     findAll(ctx: RequestContext, options?: ListQueryOptions<Order>): Promise<PaginatedList<Order>> {
@@ -321,7 +324,7 @@ export class OrderService {
         if (order.state !== 'ArrangingPayment') {
             throw new IllegalOperationError(`error.payment-may-only-be-added-in-arrangingpayment-state`);
         }
-        const payment = await this.paymentMethodService.createPayment(order, input.method, input.metadata);
+        const payment = await this.paymentMethodService.createPayment(ctx, order, input.method, input.metadata);
         order.payments = [...(order.payments || []), payment];
         await this.connection.getRepository(Order).save(order);
 
@@ -378,6 +381,14 @@ export class OrderService {
         );
 
         for (const order of orders) {
+            await this.historyService.createHistoryEntryForOrder({
+                ctx,
+                orderId: order.id,
+                type: HistoryEntryType.ORDER_FULLFILLMENT,
+                data: {
+                    fulfillmentId: fulfillment.id,
+                },
+            });
             const orderWithFulfillments = await this.connection.getRepository(Order).findOne(order.id, {
                 relations: ['lines', 'lines.items', 'lines.items.fulfillment'],
             });
@@ -386,6 +397,7 @@ export class OrderService {
             }
             const allOrderItemsFulfilled = orderWithFulfillments.lines
                 .reduce((orderItems, line) => [...orderItems, ...line.items], [] as OrderItem[])
+                .filter(orderItem => !orderItem.cancelled)
                 .every(orderItem => {
                     return !!orderItem.fulfillment;
                 });
@@ -447,6 +459,15 @@ export class OrderService {
             throw new IllegalOperationError('error.cancel-order-lines-invalid-order-state', { state: order.state });
         }
         await this.stockMovementService.createCancellationsForOrderItems(items);
+        await this.historyService.createHistoryEntryForOrder({
+            ctx,
+            orderId: order.id,
+            type: HistoryEntryType.ORDER_CANCELLATION,
+            data: {
+                orderItemIds: items.map(i => i.id),
+                reason: input.reason || undefined,
+            },
+        });
 
         const orderWithItems = await this.connection.getRepository(Order).findOne(order.id, {
             relations: ['lines', 'lines.items'],
@@ -492,13 +513,13 @@ export class OrderService {
             throw new IllegalOperationError('error.refund-order-item-already-refunded');
         }
 
-        return await this.paymentMethodService.createRefund(input, order, items, payment);
+        return await this.paymentMethodService.createRefund(ctx, input, order, items, payment);
     }
 
     async settleRefund(ctx: RequestContext, input: SettleRefundInput): Promise<Refund> {
         const refund = await getEntityOrThrow(this.connection, Refund, input.id, { relations: ['payment', 'payment.order'] });
         refund.transactionId = input.transactionId;
-        this.refundStateMachine.transition(ctx, refund.payment.order, refund, 'Settled');
+        await this.refundStateMachine.transition(ctx, refund.payment.order, refund, 'Settled');
         return this.connection.getRepository(Refund).save(refund);
     }
 

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

@@ -6,6 +6,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { Connection } from 'typeorm';
 
+import { RequestContext } from '../../api/common/request-context';
 import { UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { getConfig } from '../../config/config-helpers';
@@ -22,6 +23,8 @@ import { PaymentMethod } from '../../entity/payment-method/payment-method.entity
 import { Payment, PaymentMetadata } from '../../entity/payment/payment.entity';
 import { Refund } from '../../entity/refund/refund.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
+import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
@@ -31,6 +34,8 @@ export class PaymentMethodService {
         @InjectConnection() private connection: Connection,
         private configService: ConfigService,
         private listQueryBuilder: ListQueryBuilder,
+        private paymentStateMachine: PaymentStateMachine,
+        private refundStateMachine: RefundStateMachine,
     ) {}
 
     async initPaymentMethods() {
@@ -65,11 +70,13 @@ export class PaymentMethodService {
         return this.connection.getRepository(PaymentMethod).save(updatedPaymentMethod);
     }
 
-    async createPayment(order: Order, method: string, metadata: PaymentMetadata): Promise<Payment> {
+    async createPayment(ctx: RequestContext, order: Order, method: string, metadata: PaymentMetadata): Promise<Payment> {
         const { paymentMethod, handler } = await this.getMethodAndHandler(method);
         const result = await handler.createPayment(order, paymentMethod.configArgs, metadata || {});
-        const payment = new Payment(result);
-        return this.connection.getRepository(Payment).save(payment);
+        const payment = await this.connection.getRepository(Payment).save(new Payment({ ...result, state: 'Created' }));
+        await this.paymentStateMachine.transition(ctx, order, payment, result.state);
+        await this.connection.getRepository(Payment).save(payment);
+        return payment;
     }
 
     async settlePayment(payment: Payment, order: Order) {
@@ -77,14 +84,15 @@ export class PaymentMethodService {
         return handler.settlePayment(order, payment, paymentMethod.configArgs);
     }
 
-    async createRefund(input: RefundOrderInput, order: Order, items: OrderItem[], payment: Payment): Promise<Refund> {
+    async createRefund(ctx: RequestContext, input: RefundOrderInput, order: Order, items: OrderItem[], payment: Payment): Promise<Refund> {
         const { paymentMethod, handler } = await this.getMethodAndHandler(payment.method);
         const itemAmount = items.reduce((sum, item) => sum + item.unitPriceWithTax, 0);
         const refundAmount = itemAmount + input.shipping + input.adjustment;
-        const refund = new Refund({
+        let refund = new Refund({
             payment,
             orderItems: items,
             items: itemAmount,
+            reason: input.reason,
             adjustment: input.adjustment,
             shipping: input.shipping,
             total: refundAmount,
@@ -94,11 +102,14 @@ export class PaymentMethodService {
         });
         const createRefundResult = await handler.createRefund(input, refundAmount, order, payment, paymentMethod.configArgs);
         if (createRefundResult) {
-            refund.state = createRefundResult.state;
             refund.transactionId = createRefundResult.transactionId || '';
             refund.metadata = createRefundResult.metadata || {};
         }
-        return this.connection.getRepository(Refund).save(refund);
+        refund = await this.connection.getRepository(Refund).save(refund);
+        if (createRefundResult) {
+            await this.refundStateMachine.transition(ctx, order, refund, createRefundResult.state);
+        }
+        return refund;
     }
 
     private async getMethodAndHandler(method: string): Promise<{ paymentMethod: PaymentMethod, handler: PaymentMethodHandler }> {

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

@@ -29,7 +29,7 @@ export const devConfig: VendureConfig = {
         paymentMethodHandlers: [examplePaymentHandler],
     },
     customFields: {},
-    logger: new DefaultLogger({ level: LogLevel.Info }),
+    logger: new DefaultLogger({ level: LogLevel.Verbose }),
     importExportOptions: {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-admin.json


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-shop.json


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است