Ver código fonte

feat(core): Implement Admin API operations for stock location, e2e tests

Relates to #1545
Michael Bromley 3 anos atrás
pai
commit
7913b9ac0d
21 arquivos alterados com 1415 adições e 16 exclusões
  1. 83 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 1 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  3. 77 0
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  4. 83 0
      packages/common/src/generated-types.ts
  5. 8 0
      packages/core/e2e/fixtures/e2e-products-stock-control-multi.csv
  6. 84 0
      packages/core/e2e/fixtures/test-plugins/multi-location-stock-plugin.ts
  7. 127 0
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  8. 594 0
      packages/core/e2e/stock-control-multi-location.e2e-spec.ts
  9. 2 0
      packages/core/src/api/api-internal-modules.ts
  10. 54 0
      packages/core/src/api/resolvers/admin/stock-location.resolver.ts
  11. 9 1
      packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts
  12. 34 0
      packages/core/src/api/schema/admin-api/stock-location.api.graphql
  13. 55 11
      packages/core/src/config/catalog/default-stock-location-strategy.ts
  14. 2 1
      packages/core/src/config/config.module.ts
  15. 1 0
      packages/core/src/config/index.ts
  16. 4 3
      packages/core/src/service/services/product-variant.service.ts
  17. 8 0
      packages/core/src/service/services/stock-level.service.ts
  18. 35 0
      packages/core/src/service/services/stock-location.service.ts
  19. 77 0
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  20. 77 0
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  21. 0 0
      schema-admin.json

+ 83 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -831,6 +831,12 @@ export type CreateShippingMethodInput = {
   translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  name: Scalars['String'];
+};
+
 export type CreateTagInput = {
   value: Scalars['String'];
 };
@@ -1416,6 +1422,11 @@ export type DeleteAssetsInput = {
   force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+  id: Scalars['ID'];
+  transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
   __typename?: 'DeletionResponse';
   message?: Maybe<Scalars['String']>;
@@ -2499,6 +2510,7 @@ export type Mutation = {
   createSeller: Seller;
   /** Create a new ShippingMethod */
   createShippingMethod: ShippingMethod;
+  createStockLocation: StockLocation;
   /** Create a new Tag */
   createTag: Tag;
   /** Create a new TaxCategory */
@@ -2556,6 +2568,7 @@ export type Mutation = {
   deleteSeller: DeletionResponse;
   /** Delete a ShippingMethod */
   deleteShippingMethod: DeletionResponse;
+  deleteStockLocation: DeletionResponse;
   /** Delete an existing Tag */
   deleteTag: DeletionResponse;
   /** Deletes a TaxCategory */
@@ -2670,6 +2683,7 @@ export type Mutation = {
   updateSeller: Seller;
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod;
+  updateStockLocation: StockLocation;
   /** Update an existing Tag */
   updateTag: Tag;
   /** Update an existing TaxCategory */
@@ -2892,6 +2906,11 @@ export type MutationCreateShippingMethodArgs = {
 };
 
 
+export type MutationCreateStockLocationArgs = {
+  input: CreateStockLocationInput;
+};
+
+
 export type MutationCreateTagArgs = {
   input: CreateTagInput;
 };
@@ -3046,6 +3065,11 @@ export type MutationDeleteShippingMethodArgs = {
 };
 
 
+export type MutationDeleteStockLocationArgs = {
+  input: DeleteStockLocationInput;
+};
+
+
 export type MutationDeleteTagArgs = {
   id: Scalars['ID'];
 };
@@ -3378,6 +3402,11 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
+export type MutationUpdateStockLocationArgs = {
+  input: UpdateStockLocationInput;
+};
+
+
 export type MutationUpdateTagArgs = {
   input: UpdateTagInput;
 };
@@ -4551,6 +4580,8 @@ export type Query = {
   shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
   shippingMethod?: Maybe<ShippingMethod>;
   shippingMethods: ShippingMethodList;
+  stockLocation?: Maybe<StockLocation>;
+  stockLocations: StockLocationList;
   tag: Tag;
   tags: TagList;
   taxCategories: Array<TaxCategory>;
@@ -4775,6 +4806,16 @@ export type QueryShippingMethodsArgs = {
 };
 
 
+export type QueryStockLocationArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QueryStockLocationsArgs = {
+  options?: InputMaybe<StockLocationListOptions>;
+};
+
+
 export type QueryTagArgs = {
   id: Scalars['ID'];
 };
@@ -5287,6 +5328,41 @@ export type StockLocation = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  description?: InputMaybe<StringOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+  __typename?: 'StockLocationList';
+  items: Array<StockLocation>;
+  totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<StockLocationFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<StockLocationSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  description?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
   createdAt: Scalars['DateTime'];
   id: Scalars['ID'];
@@ -5801,6 +5877,13 @@ export type UpdateShippingMethodInput = {
   translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
   id: Scalars['ID'];
   value?: InputMaybe<Scalars['String']>;

+ 1 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -178,6 +178,7 @@ const result: PossibleTypesResultData = {
             'RoleList',
             'SellerList',
             'ShippingMethodList',
+            'StockLocationList',
             'TagList',
             'TaxRateList',
         ],

+ 77 - 0
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -817,6 +817,12 @@ export type CreateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    name: Scalars['String'];
+};
+
 export type CreateTagInput = {
     value: Scalars['String'];
 };
@@ -1392,6 +1398,11 @@ export type DeleteAssetsInput = {
     force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+    id: Scalars['ID'];
+    transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
     message?: Maybe<Scalars['String']>;
     result: DeletionResult;
@@ -2447,6 +2458,7 @@ export type Mutation = {
     createSeller: Seller;
     /** Create a new ShippingMethod */
     createShippingMethod: ShippingMethod;
+    createStockLocation: StockLocation;
     /** Create a new Tag */
     createTag: Tag;
     /** Create a new TaxCategory */
@@ -2504,6 +2516,7 @@ export type Mutation = {
     deleteSeller: DeletionResponse;
     /** Delete a ShippingMethod */
     deleteShippingMethod: DeletionResponse;
+    deleteStockLocation: DeletionResponse;
     /** Delete an existing Tag */
     deleteTag: DeletionResponse;
     /** Deletes a TaxCategory */
@@ -2608,6 +2621,7 @@ export type Mutation = {
     updateSeller: Seller;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
+    updateStockLocation: StockLocation;
     /** Update an existing Tag */
     updateTag: Tag;
     /** Update an existing TaxCategory */
@@ -2788,6 +2802,10 @@ export type MutationCreateShippingMethodArgs = {
     input: CreateShippingMethodInput;
 };
 
+export type MutationCreateStockLocationArgs = {
+    input: CreateStockLocationInput;
+};
+
 export type MutationCreateTagArgs = {
     input: CreateTagInput;
 };
@@ -2912,6 +2930,10 @@ export type MutationDeleteShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationDeleteStockLocationArgs = {
+    input: DeleteStockLocationInput;
+};
+
 export type MutationDeleteTagArgs = {
     id: Scalars['ID'];
 };
@@ -3153,6 +3175,10 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
+export type MutationUpdateStockLocationArgs = {
+    input: UpdateStockLocationInput;
+};
+
 export type MutationUpdateTagArgs = {
     input: UpdateTagInput;
 };
@@ -4266,6 +4292,8 @@ export type Query = {
     shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingMethods: ShippingMethodList;
+    stockLocation?: Maybe<StockLocation>;
+    stockLocations: StockLocationList;
     tag: Tag;
     tags: TagList;
     taxCategories: Array<TaxCategory>;
@@ -4446,6 +4474,14 @@ export type QueryShippingMethodsArgs = {
     options?: InputMaybe<ShippingMethodListOptions>;
 };
 
+export type QueryStockLocationArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryStockLocationsArgs = {
+    options?: InputMaybe<StockLocationListOptions>;
+};
+
 export type QueryTagArgs = {
     id: Scalars['ID'];
 };
@@ -4944,6 +4980,40 @@ export type StockLocation = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+    createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
+    id?: InputMaybe<IdOperators>;
+    name?: InputMaybe<StringOperators>;
+    updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+    items: Array<StockLocation>;
+    totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+    /** Allows the results to be filtered */
+    filter?: InputMaybe<StockLocationFilterParameter>;
+    /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+    filterOperator?: InputMaybe<LogicalOperator>;
+    /** Skips the first n results, for use in pagination */
+    skip?: InputMaybe<Scalars['Int']>;
+    /** Specifies which properties to sort the results by */
+    sort?: InputMaybe<StockLocationSortParameter>;
+    /** Takes n results, for use in pagination */
+    take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+    createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
+    id?: InputMaybe<SortOrder>;
+    name?: InputMaybe<SortOrder>;
+    updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
     createdAt: Scalars['DateTime'];
     id: Scalars['ID'];
@@ -5440,6 +5510,13 @@ export type UpdateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id: Scalars['ID'];
+    name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
     id: Scalars['ID'];
     value?: InputMaybe<Scalars['String']>;

+ 83 - 0
packages/common/src/generated-types.ts

@@ -831,6 +831,12 @@ export type CreateShippingMethodInput = {
   translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  name: Scalars['String'];
+};
+
 export type CreateTagInput = {
   value: Scalars['String'];
 };
@@ -1409,6 +1415,11 @@ export type DeleteAssetsInput = {
   force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+  id: Scalars['ID'];
+  transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
   __typename?: 'DeletionResponse';
   message?: Maybe<Scalars['String']>;
@@ -2492,6 +2503,7 @@ export type Mutation = {
   createSeller: Seller;
   /** Create a new ShippingMethod */
   createShippingMethod: ShippingMethod;
+  createStockLocation: StockLocation;
   /** Create a new Tag */
   createTag: Tag;
   /** Create a new TaxCategory */
@@ -2549,6 +2561,7 @@ export type Mutation = {
   deleteSeller: DeletionResponse;
   /** Delete a ShippingMethod */
   deleteShippingMethod: DeletionResponse;
+  deleteStockLocation: DeletionResponse;
   /** Delete an existing Tag */
   deleteTag: DeletionResponse;
   /** Deletes a TaxCategory */
@@ -2653,6 +2666,7 @@ export type Mutation = {
   updateSeller: Seller;
   /** Update an existing ShippingMethod */
   updateShippingMethod: ShippingMethod;
+  updateStockLocation: StockLocation;
   /** Update an existing Tag */
   updateTag: Tag;
   /** Update an existing TaxCategory */
@@ -2874,6 +2888,11 @@ export type MutationCreateShippingMethodArgs = {
 };
 
 
+export type MutationCreateStockLocationArgs = {
+  input: CreateStockLocationInput;
+};
+
+
 export type MutationCreateTagArgs = {
   input: CreateTagInput;
 };
@@ -3028,6 +3047,11 @@ export type MutationDeleteShippingMethodArgs = {
 };
 
 
+export type MutationDeleteStockLocationArgs = {
+  input: DeleteStockLocationInput;
+};
+
+
 export type MutationDeleteTagArgs = {
   id: Scalars['ID'];
 };
@@ -3325,6 +3349,11 @@ export type MutationUpdateShippingMethodArgs = {
 };
 
 
+export type MutationUpdateStockLocationArgs = {
+  input: UpdateStockLocationInput;
+};
+
+
 export type MutationUpdateTagArgs = {
   input: UpdateTagInput;
 };
@@ -4486,6 +4515,8 @@ export type Query = {
   shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
   shippingMethod?: Maybe<ShippingMethod>;
   shippingMethods: ShippingMethodList;
+  stockLocation?: Maybe<StockLocation>;
+  stockLocations: StockLocationList;
   tag: Tag;
   tags: TagList;
   taxCategories: Array<TaxCategory>;
@@ -4708,6 +4739,16 @@ export type QueryShippingMethodsArgs = {
 };
 
 
+export type QueryStockLocationArgs = {
+  id: Scalars['ID'];
+};
+
+
+export type QueryStockLocationsArgs = {
+  options?: InputMaybe<StockLocationListOptions>;
+};
+
+
 export type QueryTagArgs = {
   id: Scalars['ID'];
 };
@@ -5220,6 +5261,41 @@ export type StockLocation = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+  createdAt?: InputMaybe<DateOperators>;
+  description?: InputMaybe<StringOperators>;
+  id?: InputMaybe<IdOperators>;
+  name?: InputMaybe<StringOperators>;
+  updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+  __typename?: 'StockLocationList';
+  items: Array<StockLocation>;
+  totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+  /** Allows the results to be filtered */
+  filter?: InputMaybe<StockLocationFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: InputMaybe<LogicalOperator>;
+  /** Skips the first n results, for use in pagination */
+  skip?: InputMaybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: InputMaybe<StockLocationSortParameter>;
+  /** Takes n results, for use in pagination */
+  take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+  createdAt?: InputMaybe<SortOrder>;
+  description?: InputMaybe<SortOrder>;
+  id?: InputMaybe<SortOrder>;
+  name?: InputMaybe<SortOrder>;
+  updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
   createdAt: Scalars['DateTime'];
   id: Scalars['ID'];
@@ -5725,6 +5801,13 @@ export type UpdateShippingMethodInput = {
   translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+  customFields?: InputMaybe<Scalars['JSON']>;
+  description?: InputMaybe<Scalars['String']>;
+  id: Scalars['ID'];
+  name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
   id: Scalars['ID'];
   value?: InputMaybe<Scalars['String']>;

+ 8 - 0
packages/core/e2e/fixtures/e2e-products-stock-control-multi.csv

@@ -0,0 +1,8 @@
+name          , slug          , description                                                                                                                                                                                                                                                                                        , assets                             , facets                                  , optionGroups      , optionValues   , sku      , price   , taxCategory , stockOnHand , trackInventory , variantAssets , variantFacets
+Laptop        , laptop        , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz."                       , derick-david-409858-unsplash.jpg   , category:electronics|category:computers , "screen size|RAM" , "13 inch|8GB"  , L2201308 , 1299.00 , standard    , 100         , true           ,               ,
+              ,               ,                                                                                                                                                                                                                                                                                                    ,                                    ,                                         ,                   , "15 inch|8GB"  , L2201508 , 1399.00 , standard    , 100         , true           ,               ,
+              ,               ,                                                                                                                                                                                                                                                                                                    ,                                    ,                                         ,                   , "13 inch|16GB" , L2201316 , 2199.00 , standard    , 100         , true           ,               ,
+              ,               ,                                                                                                                                                                                                                                                                                                    ,                                    ,                                         ,                   , "15 inch|16GB" , L2201516 , 2299.00 , standard    , 100         , true           ,               ,
+Curvy Monitor , curvy-monitor , "Discover a truly immersive viewing experience with this monitor curved more deeply than any other. Wrapping around your field of vision the 1,800 R screencreates a wider field of view, enhances depth perception, and minimises peripheral distractions to draw you deeper in to your content." , alexandru-acea-686569-unsplash.jpg , category:electronics|category:computers , monitor size      , 24 inch        , C24F390  , 143.74  , standard    , 100         , true           ,               ,
+              ,               ,                                                                                                                                                                                                                                                                                                    ,                                    ,                                         ,                   , 27 inch        , C27F390  , 169.94  , standard    , 100         , true           ,               ,
+              ,               ,                                                                                                                                                                                                                                                                                                    ,                                    ,                                         ,                   , 32 inch        , C32F390  , 199.94  , standard    , 100         , true           ,               ,

+ 84 - 0
packages/core/e2e/fixtures/test-plugins/multi-location-stock-plugin.ts

@@ -0,0 +1,84 @@
+import {
+    AvailableStock,
+    DefaultStockLocationStrategy,
+    ID,
+    idsAreEqual,
+    Injector,
+    LocationWithQuantity,
+    OrderLine,
+    PluginCommonModule,
+    ProductVariant,
+    RequestContext,
+    StockDisplayStrategy,
+    StockLevel,
+    StockLocation,
+    TransactionalConnection,
+    VendurePlugin,
+} from '@vendure/core';
+
+declare module '@vendure/core/dist/entity/custom-entity-fields' {
+    interface CustomOrderLineFields {
+        stockLocationId?: string;
+    }
+}
+
+export class TestStockLocationStrategy extends DefaultStockLocationStrategy {
+    private connection: TransactionalConnection;
+
+    init(injector: Injector) {
+        this.connection = injector.get(TransactionalConnection);
+    }
+
+    forAllocation(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
+        const selectedLocation = stockLocations.find(location =>
+            idsAreEqual(location.id, orderLine.customFields.stockLocationId),
+        );
+        return [{ location: selectedLocation ?? stockLocations[0], quantity }];
+    }
+
+    getAvailableStock(
+        ctx: RequestContext,
+        productVariantId: ID,
+        stockLevels: StockLevel[],
+    ): AvailableStock | Promise<AvailableStock> {
+        const locationId = ctx.req?.query.fromLocation;
+        const locationStock =
+            locationId &&
+            stockLevels.find(level => idsAreEqual(level.stockLocationId, locationId.toString()));
+        if (locationStock) {
+            return {
+                stockOnHand: locationStock.stockOnHand,
+                stockAllocated: locationStock.stockAllocated,
+            };
+        }
+        return stockLevels.reduce(
+            (all, level) => ({
+                stockOnHand: all.stockOnHand + level.stockOnHand,
+                stockAllocated: all.stockAllocated + level.stockAllocated,
+            }),
+            { stockOnHand: 0, stockAllocated: 0 },
+        );
+    }
+}
+
+export class TestStockDisplayStrategy implements StockDisplayStrategy {
+    getStockLevel(ctx: RequestContext, productVariant: ProductVariant, saleableStockLevel: number) {
+        return saleableStockLevel.toString();
+    }
+}
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    configuration: config => {
+        config.catalogOptions.stockLocationStrategy = new TestStockLocationStrategy();
+        config.catalogOptions.stockDisplayStrategy = new TestStockDisplayStrategy();
+        config.customFields.OrderLine.push({ name: 'stockLocationId', type: 'string', nullable: true });
+        return config;
+    },
+})
+export class TestMultiLocationStockPlugin {}

+ 127 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -817,6 +817,12 @@ export type CreateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    name: Scalars['String'];
+};
+
 export type CreateTagInput = {
     value: Scalars['String'];
 };
@@ -1392,6 +1398,11 @@ export type DeleteAssetsInput = {
     force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+    id: Scalars['ID'];
+    transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
     message?: Maybe<Scalars['String']>;
     result: DeletionResult;
@@ -2447,6 +2458,7 @@ export type Mutation = {
     createSeller: Seller;
     /** Create a new ShippingMethod */
     createShippingMethod: ShippingMethod;
+    createStockLocation: StockLocation;
     /** Create a new Tag */
     createTag: Tag;
     /** Create a new TaxCategory */
@@ -2504,6 +2516,7 @@ export type Mutation = {
     deleteSeller: DeletionResponse;
     /** Delete a ShippingMethod */
     deleteShippingMethod: DeletionResponse;
+    deleteStockLocation: DeletionResponse;
     /** Delete an existing Tag */
     deleteTag: DeletionResponse;
     /** Deletes a TaxCategory */
@@ -2608,6 +2621,7 @@ export type Mutation = {
     updateSeller: Seller;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
+    updateStockLocation: StockLocation;
     /** Update an existing Tag */
     updateTag: Tag;
     /** Update an existing TaxCategory */
@@ -2788,6 +2802,10 @@ export type MutationCreateShippingMethodArgs = {
     input: CreateShippingMethodInput;
 };
 
+export type MutationCreateStockLocationArgs = {
+    input: CreateStockLocationInput;
+};
+
 export type MutationCreateTagArgs = {
     input: CreateTagInput;
 };
@@ -2912,6 +2930,10 @@ export type MutationDeleteShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationDeleteStockLocationArgs = {
+    input: DeleteStockLocationInput;
+};
+
 export type MutationDeleteTagArgs = {
     id: Scalars['ID'];
 };
@@ -3153,6 +3175,10 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
+export type MutationUpdateStockLocationArgs = {
+    input: UpdateStockLocationInput;
+};
+
 export type MutationUpdateTagArgs = {
     input: UpdateTagInput;
 };
@@ -4266,6 +4292,8 @@ export type Query = {
     shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingMethods: ShippingMethodList;
+    stockLocation?: Maybe<StockLocation>;
+    stockLocations: StockLocationList;
     tag: Tag;
     tags: TagList;
     taxCategories: Array<TaxCategory>;
@@ -4446,6 +4474,14 @@ export type QueryShippingMethodsArgs = {
     options?: InputMaybe<ShippingMethodListOptions>;
 };
 
+export type QueryStockLocationArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryStockLocationsArgs = {
+    options?: InputMaybe<StockLocationListOptions>;
+};
+
 export type QueryTagArgs = {
     id: Scalars['ID'];
 };
@@ -4944,6 +4980,40 @@ export type StockLocation = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+    createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
+    id?: InputMaybe<IdOperators>;
+    name?: InputMaybe<StringOperators>;
+    updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+    items: Array<StockLocation>;
+    totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+    /** Allows the results to be filtered */
+    filter?: InputMaybe<StockLocationFilterParameter>;
+    /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+    filterOperator?: InputMaybe<LogicalOperator>;
+    /** Skips the first n results, for use in pagination */
+    skip?: InputMaybe<Scalars['Int']>;
+    /** Specifies which properties to sort the results by */
+    sort?: InputMaybe<StockLocationSortParameter>;
+    /** Takes n results, for use in pagination */
+    take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+    createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
+    id?: InputMaybe<SortOrder>;
+    name?: InputMaybe<SortOrder>;
+    updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
     createdAt: Scalars['DateTime'];
     id: Scalars['ID'];
@@ -5440,6 +5510,13 @@ export type UpdateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id: Scalars['ID'];
+    name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
     id: Scalars['ID'];
     value?: InputMaybe<Scalars['String']>;
@@ -10759,6 +10836,56 @@ export type GetCustomerIdsQueryVariables = Exact<{ [key: string]: never }>;
 
 export type GetCustomerIdsQuery = { customers: { items: Array<{ id: string }> } };
 
+export type StockLocationFragment = { id: string; name: string; description: string };
+
+export type GetStockLocationQueryVariables = Exact<{
+    id: Scalars['ID'];
+}>;
+
+export type GetStockLocationQuery = {
+    stockLocation?: { id: string; name: string; description: string } | null;
+};
+
+export type GetStockLocationsQueryVariables = Exact<{
+    options?: InputMaybe<StockLocationListOptions>;
+}>;
+
+export type GetStockLocationsQuery = {
+    stockLocations: { totalItems: number; items: Array<{ id: string; name: string; description: string }> };
+};
+
+export type CreateStockLocationMutationVariables = Exact<{
+    input: CreateStockLocationInput;
+}>;
+
+export type CreateStockLocationMutation = {
+    createStockLocation: { id: string; name: string; description: string };
+};
+
+export type UpdateStockLocationMutationVariables = Exact<{
+    input: UpdateStockLocationInput;
+}>;
+
+export type UpdateStockLocationMutation = {
+    updateStockLocation: { id: string; name: string; description: string };
+};
+
+export type GetVariantStockLevelsQueryVariables = Exact<{
+    options?: InputMaybe<ProductVariantListOptions>;
+}>;
+
+export type GetVariantStockLevelsQuery = {
+    productVariants: {
+        items: Array<{
+            id: string;
+            name: string;
+            stockOnHand: number;
+            stockAllocated: number;
+            stockLevels: Array<{ stockLocationId: string; stockOnHand: number; stockAllocated: number }>;
+        }>;
+    };
+};
+
 export type UpdateStockMutationVariables = Exact<{
     input: Array<UpdateProductVariantInput> | UpdateProductVariantInput;
 }>;

+ 594 - 0
packages/core/e2e/stock-control-multi-location.e2e-spec.ts

@@ -0,0 +1,594 @@
+/* tslint:disable:no-non-null-assertion */
+import { manualFulfillmentHandler, mergeConfig } from '@vendure/core';
+import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { initialData } from '../../../e2e-common/e2e-initial-data';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
+
+import { testSuccessfulPaymentMethod, twoStagePaymentMethod } from './fixtures/test-payment-methods';
+import {
+    TestMultiLocationStockPlugin,
+    TestStockDisplayStrategy,
+    TestStockLocationStrategy,
+} from './fixtures/test-plugins/multi-location-stock-plugin';
+import * as Codegen from './graphql/generated-e2e-admin-types';
+import { CreateAddressInput, FulfillmentFragment } from './graphql/generated-e2e-admin-types';
+import { PaymentInput } from './graphql/generated-e2e-shop-types';
+import * as CodegenShop from './graphql/generated-e2e-shop-types';
+import {
+    CANCEL_ORDER,
+    CREATE_FULFILLMENT,
+    GET_ORDER,
+    GET_STOCK_MOVEMENT,
+    UPDATE_GLOBAL_SETTINGS,
+    UPDATE_PRODUCT_VARIANTS,
+} from './graphql/shared-definitions';
+import {
+    ADD_ITEM_TO_ORDER,
+    ADD_PAYMENT,
+    GET_ELIGIBLE_SHIPPING_METHODS,
+    GET_PRODUCT_WITH_STOCK_LEVEL,
+    SET_SHIPPING_ADDRESS,
+    SET_SHIPPING_METHOD,
+    TRANSITION_TO_STATE,
+} from './graphql/shop-definitions';
+
+describe('Stock control', () => {
+    let defaultStockLocationId: string;
+    let secondStockLocationId: string;
+    const { server, adminClient, shopClient } = createTestEnvironment(
+        mergeConfig(testConfig(), {
+            paymentOptions: {
+                paymentMethodHandlers: [testSuccessfulPaymentMethod],
+            },
+            plugins: [TestMultiLocationStockPlugin],
+        }),
+    );
+
+    const orderGuard: ErrorResultGuard<
+        CodegenShop.TestOrderFragmentFragment | CodegenShop.UpdatedOrderFragment
+    > = createErrorResultGuard(input => !!input.lines);
+
+    const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard(
+        input => !!input.state,
+    );
+
+    async function getProductWithStockMovement(productId: string) {
+        const { product } = await adminClient.query<
+            Codegen.GetStockMovementQuery,
+            Codegen.GetStockMovementQueryVariables
+        >(GET_STOCK_MOVEMENT, { id: productId });
+        return product;
+    }
+
+    async function setFirstEligibleShippingMethod() {
+        const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
+            GET_ELIGIBLE_SHIPPING_METHODS,
+        );
+        await shopClient.query<
+            CodegenShop.SetShippingMethodMutation,
+            CodegenShop.SetShippingMethodMutationVariables
+        >(SET_SHIPPING_METHOD, {
+            id: eligibleShippingMethods[0].id,
+        });
+    }
+
+    beforeAll(async () => {
+        await server.init({
+            initialData: {
+                ...initialData,
+                paymentMethods: [
+                    {
+                        name: testSuccessfulPaymentMethod.code,
+                        handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
+                    },
+                    {
+                        name: twoStagePaymentMethod.code,
+                        handler: { code: twoStagePaymentMethod.code, arguments: [] },
+                    },
+                ],
+            },
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control-multi.csv'),
+            customerCount: 3,
+        });
+        await adminClient.asSuperAdmin();
+
+        await adminClient.query<
+            Codegen.UpdateGlobalSettingsMutation,
+            Codegen.UpdateGlobalSettingsMutationVariables
+        >(UPDATE_GLOBAL_SETTINGS, {
+            input: {
+                trackInventory: false,
+            },
+        });
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    it('default StockLocation exists', async () => {
+        const { stockLocations } = await adminClient.query<Codegen.GetStockLocationsQuery>(
+            GET_STOCK_LOCATIONS,
+        );
+        expect(stockLocations.items.length).toBe(1);
+        expect(stockLocations.items[0].name).toBe('Default Stock Location');
+        defaultStockLocationId = stockLocations.items[0].id;
+    });
+
+    it('variant stock is all at default StockLocation', async () => {
+        const { productVariants } = await adminClient.query<
+            Codegen.GetVariantStockLevelsQuery,
+            Codegen.GetVariantStockLevelsQueryVariables
+        >(GET_VARIANT_STOCK_LEVELS, {});
+
+        expect(productVariants.items.every(variant => variant.stockLevels.length === 1)).toBe(true);
+        expect(
+            productVariants.items.every(
+                variant => variant.stockLevels[0].stockLocationId === defaultStockLocationId,
+            ),
+        ).toBe(true);
+    });
+
+    it('create StockLocation', async () => {
+        const { createStockLocation } = await adminClient.query<
+            Codegen.CreateStockLocationMutation,
+            Codegen.CreateStockLocationMutationVariables
+        >(CREATE_STOCK_LOCATION, {
+            input: {
+                name: 'StockLocation1',
+                description: 'StockLocation1',
+            },
+        });
+
+        expect(createStockLocation).toEqual({
+            id: 'T_2',
+            name: 'StockLocation1',
+            description: 'StockLocation1',
+        });
+        secondStockLocationId = createStockLocation.id;
+    });
+
+    it('update StockLocation', async () => {
+        const { updateStockLocation } = await adminClient.query<
+            Codegen.UpdateStockLocationMutation,
+            Codegen.UpdateStockLocationMutationVariables
+        >(UPDATE_STOCK_LOCATION, {
+            input: {
+                id: 'T_2',
+                name: 'Warehouse 2',
+                description: 'The secondary warehouse',
+            },
+        });
+
+        expect(updateStockLocation).toEqual({
+            id: 'T_2',
+            name: 'Warehouse 2',
+            description: 'The secondary warehouse',
+        });
+    });
+
+    it('update ProductVariants with stock levels in second location', async () => {
+        const { productVariants } = await adminClient.query<
+            Codegen.GetVariantStockLevelsQuery,
+            Codegen.GetVariantStockLevelsQueryVariables
+        >(GET_VARIANT_STOCK_LEVELS, {});
+
+        const { updateProductVariants } = await adminClient.query<
+            Codegen.UpdateProductVariantsMutation,
+            Codegen.UpdateProductVariantsMutationVariables
+        >(UPDATE_PRODUCT_VARIANTS, {
+            input: productVariants.items.map(variant => ({
+                id: variant.id,
+                stockLevels: [{ stockLocationId: secondStockLocationId, stockOnHand: 120 }],
+            })),
+        });
+
+        const {
+            productVariants: { items },
+        } = await adminClient.query<
+            Codegen.GetVariantStockLevelsQuery,
+            Codegen.GetVariantStockLevelsQueryVariables
+        >(GET_VARIANT_STOCK_LEVELS, {});
+        expect(items.every(variant => variant.stockLevels.length === 2)).toBe(true);
+        expect(
+            items.every(variant => {
+                return (
+                    variant.stockLevels[0].stockLocationId === defaultStockLocationId &&
+                    variant.stockLevels[1].stockLocationId === secondStockLocationId
+                );
+            }),
+        ).toBe(true);
+    });
+
+    it('StockLocationStrategy.getAvailableStock() is used to calculate saleable stock level', async () => {
+        const result1 = await shopClient.query<
+            CodegenShop.GetProductStockLevelQuery,
+            CodegenShop.GetProductStockLevelQueryVariables
+        >(GET_PRODUCT_WITH_STOCK_LEVEL, {
+            id: 'T_1',
+        });
+
+        expect(result1.product?.variants[0].stockLevel).toBe('220');
+
+        const result2 = await shopClient.query<
+            CodegenShop.GetProductStockLevelQuery,
+            CodegenShop.GetProductStockLevelQueryVariables
+        >(
+            GET_PRODUCT_WITH_STOCK_LEVEL,
+            {
+                id: 'T_1',
+            },
+            { fromLocation: 1 },
+        );
+
+        expect(result2.product?.variants[0].stockLevel).toBe('100');
+
+        const result3 = await shopClient.query<
+            CodegenShop.GetProductStockLevelQuery,
+            CodegenShop.GetProductStockLevelQueryVariables
+        >(
+            GET_PRODUCT_WITH_STOCK_LEVEL,
+            {
+                id: 'T_1',
+            },
+            { fromLocation: 2 },
+        );
+
+        expect(result3.product?.variants[0].stockLevel).toBe('120');
+    });
+
+    describe('stock movements', () => {
+        const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = `
+            mutation AddItemToOrderWithCustomFields(
+                $productVariantId: ID!
+                $quantity: Int!
+                $customFields: OrderLineCustomFieldsInput
+            ) {
+                addItemToOrder(
+                    productVariantId: $productVariantId
+                    quantity: $quantity
+                    customFields: $customFields
+                ) {
+                    ... on Order {
+                        id
+                        lines { id }
+                    }
+                    ... on ErrorResult {
+                        errorCode
+                        message
+                    }
+                }
+            }
+        `;
+        let orderId: string;
+
+        it('creates Allocations according to StockLocationStrategy', async () => {
+            await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+
+            await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_1',
+                quantity: 2,
+                customFields: {
+                    stockLocationId: '1',
+                },
+            });
+            const { addItemToOrder } = await shopClient.query(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: 'T_2',
+                quantity: 2,
+                customFields: {
+                    stockLocationId: '2',
+                },
+            });
+
+            orderId = addItemToOrder.id;
+            expect(addItemToOrder.lines.length).toBe(2);
+
+            // Do all steps to check out
+            await shopClient.query<
+                CodegenShop.SetShippingAddressMutation,
+                CodegenShop.SetShippingAddressMutationVariables
+            >(SET_SHIPPING_ADDRESS, {
+                input: {
+                    streetLine1: '1 Test Street',
+                    countryCode: 'GB',
+                } as CreateAddressInput,
+            });
+            const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
+                GET_ELIGIBLE_SHIPPING_METHODS,
+            );
+            await shopClient.query<
+                CodegenShop.SetShippingMethodMutation,
+                CodegenShop.SetShippingMethodMutationVariables
+            >(SET_SHIPPING_METHOD, {
+                id: eligibleShippingMethods[0].id,
+            });
+            await shopClient.query<
+                CodegenShop.TransitionToStateMutation,
+                CodegenShop.TransitionToStateMutationVariables
+            >(TRANSITION_TO_STATE, {
+                state: 'ArrangingPayment',
+            });
+            const { addPaymentToOrder: order } = await shopClient.query<
+                CodegenShop.AddPaymentToOrderMutation,
+                CodegenShop.AddPaymentToOrderMutationVariables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: testSuccessfulPaymentMethod.code,
+                    metadata: {},
+                } as PaymentInput,
+            });
+            orderGuard.assertSuccess(order);
+
+            const { productVariants } = await adminClient.query<
+                Codegen.GetVariantStockLevelsQuery,
+                Codegen.GetVariantStockLevelsQueryVariables
+            >(GET_VARIANT_STOCK_LEVELS, {
+                options: {
+                    filter: {
+                        id: { in: ['T_1', 'T_2'] },
+                    },
+                },
+            });
+
+            // First variant gets stock allocated from location 1
+            expect(productVariants.items.find(v => v.id === 'T_1')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 2,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 0,
+                },
+            ]);
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_2')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 2,
+                },
+            ]);
+        });
+
+        it('creates Releases according to StockLocationStrategy', async () => {
+            const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
+                GET_ORDER,
+                { id: orderId },
+            );
+
+            const { cancelOrder } = await adminClient.query<
+                Codegen.CancelOrderMutation,
+                Codegen.CancelOrderMutationVariables
+            >(CANCEL_ORDER, {
+                input: {
+                    orderId,
+                    lines: order?.lines
+                        .filter(l => l.productVariant.id === 'T_2')
+                        .map(l => ({
+                            orderLineId: l.id,
+                            quantity: 1,
+                        })),
+                },
+            });
+
+            const { productVariants } = await adminClient.query<
+                Codegen.GetVariantStockLevelsQuery,
+                Codegen.GetVariantStockLevelsQueryVariables
+            >(GET_VARIANT_STOCK_LEVELS, {
+                options: {
+                    filter: {
+                        id: { eq: 'T_2' },
+                    },
+                },
+            });
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_2')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 1,
+                },
+            ]);
+        });
+
+        it('creates Sales according to StockLocationStrategy', async () => {
+            const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
+                GET_ORDER,
+                { id: orderId },
+            );
+            await adminClient.query<
+                Codegen.CreateFulfillmentMutation,
+                Codegen.CreateFulfillmentMutationVariables
+            >(CREATE_FULFILLMENT, {
+                input: {
+                    handler: {
+                        code: manualFulfillmentHandler.code,
+                        arguments: [{ name: 'method', value: 'Test1' }],
+                    },
+                    lines: order!.lines.map(l => ({
+                        orderLineId: l.id,
+                        quantity: l.quantity,
+                    })),
+                },
+            });
+
+            const { productVariants } = await adminClient.query<
+                Codegen.GetVariantStockLevelsQuery,
+                Codegen.GetVariantStockLevelsQueryVariables
+            >(GET_VARIANT_STOCK_LEVELS, {
+                options: {
+                    filter: {
+                        id: { in: ['T_1', 'T_2'] },
+                    },
+                },
+            });
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_1')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 98,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 0,
+                },
+            ]);
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_2')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 119,
+                    stockAllocated: 0,
+                },
+            ]);
+        });
+
+        it('creates Cancellations according to StockLocationStrategy', async () => {
+            const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
+                GET_ORDER,
+                { id: orderId },
+            );
+            await adminClient.query<Codegen.CancelOrderMutation, Codegen.CancelOrderMutationVariables>(
+                CANCEL_ORDER,
+                {
+                    input: {
+                        orderId,
+                        cancelShipping: true,
+                        reason: 'No longer needed',
+                    },
+                },
+            );
+
+            const { productVariants } = await adminClient.query<
+                Codegen.GetVariantStockLevelsQuery,
+                Codegen.GetVariantStockLevelsQueryVariables
+            >(GET_VARIANT_STOCK_LEVELS, {
+                options: {
+                    filter: {
+                        id: { in: ['T_1', 'T_2'] },
+                    },
+                },
+            });
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_1')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 0,
+                },
+            ]);
+
+            // Second variant gets stock allocated from location 2
+            expect(productVariants.items.find(v => v.id === 'T_2')?.stockLevels).toEqual([
+                {
+                    stockLocationId: 'T_1',
+                    stockOnHand: 100,
+                    stockAllocated: 0,
+                },
+                {
+                    stockLocationId: 'T_2',
+                    stockOnHand: 120,
+                    stockAllocated: 0,
+                },
+            ]);
+        });
+    });
+});
+
+const STOCK_LOCATION_FRAGMENT = gql`
+    fragment StockLocation on StockLocation {
+        id
+        name
+        description
+    }
+`;
+
+const GET_STOCK_LOCATION = gql`
+    query GetStockLocation($id: ID!) {
+        stockLocation(id: $id) {
+            ...StockLocation
+        }
+    }
+    ${STOCK_LOCATION_FRAGMENT}
+`;
+
+const GET_STOCK_LOCATIONS = gql`
+    query GetStockLocations($options: StockLocationListOptions) {
+        stockLocations(options: $options) {
+            items {
+                ...StockLocation
+            }
+            totalItems
+        }
+    }
+    ${STOCK_LOCATION_FRAGMENT}
+`;
+
+const CREATE_STOCK_LOCATION = gql`
+    mutation CreateStockLocation($input: CreateStockLocationInput!) {
+        createStockLocation(input: $input) {
+            ...StockLocation
+        }
+    }
+    ${STOCK_LOCATION_FRAGMENT}
+`;
+
+const UPDATE_STOCK_LOCATION = gql`
+    mutation UpdateStockLocation($input: UpdateStockLocationInput!) {
+        updateStockLocation(input: $input) {
+            ...StockLocation
+        }
+    }
+    ${STOCK_LOCATION_FRAGMENT}
+`;
+
+const GET_VARIANT_STOCK_LEVELS = gql`
+    query GetVariantStockLevels($options: ProductVariantListOptions) {
+        productVariants(options: $options) {
+            items {
+                id
+                name
+                stockOnHand
+                stockAllocated
+                stockLevels {
+                    stockLocationId
+                    stockOnHand
+                    stockAllocated
+                }
+            }
+        }
+    }
+`;

+ 2 - 0
packages/core/src/api/api-internal-modules.ts

@@ -33,6 +33,7 @@ import { RoleResolver } from './resolvers/admin/role.resolver';
 import { SearchResolver } from './resolvers/admin/search.resolver';
 import { SellerResolver } from './resolvers/admin/seller.resolver';
 import { ShippingMethodResolver } from './resolvers/admin/shipping-method.resolver';
+import { StockLocationResolver } from './resolvers/admin/stock-location.resolver';
 import { TagResolver } from './resolvers/admin/tag.resolver';
 import { TaxCategoryResolver } from './resolvers/admin/tax-category.resolver';
 import { TaxRateResolver } from './resolvers/admin/tax-rate.resolver';
@@ -108,6 +109,7 @@ const adminResolvers = [
     RoleResolver,
     SearchResolver,
     ShippingMethodResolver,
+    StockLocationResolver,
     TagResolver,
     TaxCategoryResolver,
     TaxRateResolver,

+ 54 - 0
packages/core/src/api/resolvers/admin/stock-location.resolver.ts

@@ -0,0 +1,54 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import {
+    MutationCreateStockLocationArgs,
+    MutationDeleteStockLocationArgs,
+    MutationUpdateStockLocationArgs,
+    Permission,
+    QueryStockLocationArgs,
+    QueryStockLocationsArgs,
+    StockLocationList,
+} from '@vendure/common/lib/generated-types';
+
+import { StockLocationService } from '../../../service/services/stock-location.service';
+import { RequestContext } from '../../common/request-context';
+import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
+import { Transaction } from '../../decorators/transaction.decorator';
+
+@Resolver()
+export class StockLocationResolver {
+    constructor(private stockLocationService: StockLocationService) {}
+
+    @Query()
+    @Allow(Permission.ReadCatalog, Permission.ReadProduct)
+    stockLocation(@Ctx() ctx: RequestContext, @Args() args: QueryStockLocationArgs) {
+        return this.stockLocationService.findOne(ctx, args.id);
+    }
+
+    @Query()
+    @Allow(Permission.ReadCatalog, Permission.ReadProduct)
+    stockLocations(@Ctx() ctx: RequestContext, @Args() args: QueryStockLocationsArgs) {
+        return this.stockLocationService.findAll(ctx, args.options);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.CreateCatalog, Permission.CreateProduct)
+    createStockLocation(@Ctx() ctx: RequestContext, @Args() args: MutationCreateStockLocationArgs) {
+        return this.stockLocationService.create(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.UpdateCatalog, Permission.UpdateProduct)
+    updateStockLocation(@Ctx() ctx: RequestContext, @Args() args: MutationUpdateStockLocationArgs) {
+        return this.stockLocationService.update(ctx, args.input);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.DeleteCatalog, Permission.DeleteProduct)
+    deleteStockLocation(@Ctx() ctx: RequestContext, @Args() args: MutationDeleteStockLocationArgs) {
+        return this.stockLocationService.delete(ctx, args.input);
+    }
+}

+ 9 - 1
packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts

@@ -6,7 +6,7 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
-import { Asset, Channel, FacetValue, Product, ProductOption, TaxRate } from '../../../entity';
+import { Asset, Channel, FacetValue, Product, ProductOption, StockLevel, TaxRate } from '../../../entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { StockMovement } from '../../../entity/stock-movement/stock-movement.entity';
 import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
@@ -195,4 +195,12 @@ export class ProductVariantAdminEntityResolver {
         const channels = await this.productVariantService.getProductVariantChannels(ctx, productVariant.id);
         return channels.filter(channel => (isDefaultChannel ? true : idsAreEqual(channel.id, ctx.channelId)));
     }
+
+    @ResolveField()
+    async stockLevels(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+    ): Promise<StockLevel[]> {
+        return this.stockLevelService.getStockLevelsForVariant(ctx, productVariant.id);
+    }
 }

+ 34 - 0
packages/core/src/api/schema/admin-api/stock-location.api.graphql

@@ -0,0 +1,34 @@
+type Query {
+    stockLocation(id: ID!): StockLocation
+    stockLocations(options: StockLocationListOptions): StockLocationList!
+}
+
+type Mutation {
+    createStockLocation(input: CreateStockLocationInput!): StockLocation!
+    updateStockLocation(input: UpdateStockLocationInput!): StockLocation!
+    deleteStockLocation(input: DeleteStockLocationInput!): DeletionResponse!
+}
+
+# Generated at runtime
+input StockLocationListOptions
+
+type StockLocationList implements PaginatedList {
+    items: [StockLocation!]!
+    totalItems: Int!
+}
+
+input CreateStockLocationInput {
+    name: String!
+    description: String
+}
+
+input UpdateStockLocationInput {
+    id: ID!
+    name: String
+    description: String
+}
+
+input DeleteStockLocationInput {
+    id: ID!
+    transferToLocationId: ID
+}

+ 55 - 11
packages/core/src/config/catalog/default-stock-location-strategy.ts

@@ -1,7 +1,12 @@
 import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/index';
-import { OrderLine, StockLevel, StockLocation } from '../../entity/index';
+import { idsAreEqual, Injector } from '../../common/index';
+import { TransactionalConnection } from '../../connection/index';
+import { OrderLine } from '../../entity/order-line/order-line.entity';
+import { StockLevel } from '../../entity/stock-level/stock-level.entity';
+import { StockLocation } from '../../entity/stock-location/stock-location.entity';
+import { Allocation } from '../../entity/stock-movement/allocation.entity';
 
 import { AvailableStock, LocationWithQuantity, StockLocationStrategy } from './stock-location-strategy';
 
@@ -14,6 +19,12 @@ import { AvailableStock, LocationWithQuantity, StockLocationStrategy } from './s
  * @since 2.0.0
  */
 export class DefaultStockLocationStrategy implements StockLocationStrategy {
+    private connection: TransactionalConnection;
+
+    init(injector: Injector) {
+        this.connection = injector.get(TransactionalConnection);
+    }
+
     getAvailableStock(ctx: RequestContext, productVariantId: ID, stockLevels: StockLevel[]): AvailableStock {
         let stockOnHand = 0;
         let stockAllocated = 0;
@@ -29,34 +40,67 @@ export class DefaultStockLocationStrategy implements StockLocationStrategy {
         stockLocations: StockLocation[],
         orderLine: OrderLine,
         quantity: number,
-    ) {
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
         return [{ location: stockLocations[0], quantity }];
     }
 
-    forRelease(
+    async forCancellation(
         ctx: RequestContext,
         stockLocations: StockLocation[],
         orderLine: OrderLine,
         quantity: number,
-    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
-        return [{ location: stockLocations[0], quantity }];
+    ): Promise<LocationWithQuantity[]> {
+        return this.getLocationsBasedOnAllocations(ctx, stockLocations, orderLine, quantity);
     }
 
-    forSale(
+    async forRelease(
         ctx: RequestContext,
         stockLocations: StockLocation[],
         orderLine: OrderLine,
         quantity: number,
-    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
-        return [{ location: stockLocations[0], quantity }];
+    ): Promise<LocationWithQuantity[]> {
+        return this.getLocationsBasedOnAllocations(ctx, stockLocations, orderLine, quantity);
     }
 
-    forCancellation(
+    async forSale(
         ctx: RequestContext,
         stockLocations: StockLocation[],
         orderLine: OrderLine,
         quantity: number,
-    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
-        return [{ location: stockLocations[0], quantity }];
+    ): Promise<LocationWithQuantity[]> {
+        return this.getLocationsBasedOnAllocations(ctx, stockLocations, orderLine, quantity);
+    }
+
+    private async getLocationsBasedOnAllocations(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ) {
+        const allocations = await this.connection.getRepository(ctx, Allocation).find({
+            where: {
+                orderLine,
+            },
+        });
+        let unallocated = quantity;
+        const quantityByLocationId = new Map<ID, number>();
+        for (const allocation of allocations) {
+            if (unallocated <= 0) {
+                break;
+            }
+            const qtyAtLocation = quantityByLocationId.get(allocation.stockLocationId);
+            const qtyToAdd = Math.min(allocation.quantity, unallocated);
+            if (qtyAtLocation != null) {
+                quantityByLocationId.set(allocation.stockLocationId, qtyAtLocation + qtyToAdd);
+            } else {
+                quantityByLocationId.set(allocation.stockLocationId, qtyToAdd);
+            }
+            unallocated -= qtyToAdd;
+        }
+        return [...quantityByLocationId.entries()].map(([locationId, qty]) => ({
+            // tslint:disable-next-line:no-non-null-assertion
+            location: stockLocations.find(l => idsAreEqual(l.id, locationId))!,
+            quantity: qty,
+        }));
     }
 }

+ 2 - 1
packages/core/src/config/config.module.ts

@@ -67,7 +67,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
     private getInjectableStrategies(): InjectableStrategy[] {
         const { assetNamingStrategy, assetPreviewStrategy, assetStorageStrategy } =
             this.configService.assetOptions;
-        const { productVariantPriceCalculationStrategy, stockDisplayStrategy } =
+        const { productVariantPriceCalculationStrategy, stockDisplayStrategy, stockLocationStrategy } =
             this.configService.catalogOptions;
         const {
             adminAuthenticationStrategy,
@@ -134,6 +134,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             ...(Array.isArray(activeOrderStrategy) ? activeOrderStrategy : [activeOrderStrategy]),
             orderSellerStrategy,
             shippingLineAssignmentStrategy,
+            stockLocationStrategy,
         ];
     }
 

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

@@ -12,6 +12,7 @@ export * from './auth/password-validation-strategy';
 export * from './catalog/collection-filter';
 export * from './catalog/default-collection-filters';
 export * from './catalog/default-stock-display-strategy';
+export * from './catalog/default-stock-location-strategy';
 export * from './catalog/product-variant-price-calculation-strategy';
 export * from './catalog/stock-display-strategy';
 export * from './catalog/stock-location-strategy';

+ 4 - 3
packages/core/src/service/services/product-variant.service.ts

@@ -447,13 +447,14 @@ export class ProductVariantService {
         if (input.stockOnHand && input.stockOnHand < outOfStockThreshold) {
             throw new UserInputError('error.stockonhand-cannot-be-negative');
         }
-        const inputWithoutPrice = {
+        const inputWithoutPriceAndStockLevels = {
             ...input,
         };
-        delete inputWithoutPrice.price;
+        delete inputWithoutPriceAndStockLevels.price;
+        delete inputWithoutPriceAndStockLevels.stockLevels;
         const updatedVariant = await this.translatableSaver.update({
             ctx,
-            input: inputWithoutPrice,
+            input: inputWithoutPriceAndStockLevels,
             entityType: ProductVariant,
             translationType: ProductVariantTranslation,
             beforeSave: async v => {

+ 8 - 0
packages/core/src/service/services/stock-level.service.ts

@@ -49,6 +49,14 @@ export class StockLevelService {
         );
     }
 
+    async getStockLevelsForVariant(ctx: RequestContext, productVariantId: ID): Promise<StockLevel[]> {
+        return this.connection.getRepository(ctx, StockLevel).find({
+            where: {
+                productVariantId,
+            },
+        });
+    }
+
     /**
      * @description
      * Returns the available stock (on hand and allocated) for the given {@link ProductVariant}. This is determined

+ 35 - 0
packages/core/src/service/services/stock-location.service.ts

@@ -1,4 +1,9 @@
 import { Injectable } from '@nestjs/common';
+import {
+    CreateStockLocationInput,
+    DeleteStockLocationInput,
+    UpdateStockLocationInput,
+} from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RelationPaths, RequestContext } from '../../api/index';
@@ -28,6 +33,10 @@ export class StockLocationService {
         await this.ensureDefaultStockLocationExists();
     }
 
+    findOne(ctx: RequestContext, stockLocationId: ID): Promise<StockLocation | undefined> {
+        return this.connection.getRepository(ctx, StockLocation).findOne(stockLocationId);
+    }
+
     findAll(
         ctx: RequestContext,
         options?: ListQueryOptions<StockLocation>,
@@ -45,6 +54,32 @@ export class StockLocationService {
             }));
     }
 
+    create(ctx: RequestContext, input: CreateStockLocationInput): Promise<StockLocation> {
+        return this.connection.getRepository(ctx, StockLocation).save(
+            new StockLocation({
+                name: input.name,
+                description: input.description,
+            }),
+        );
+    }
+
+    async update(ctx: RequestContext, input: UpdateStockLocationInput): Promise<StockLocation> {
+        const stockLocation = await this.connection.getEntityOrThrow(ctx, StockLocation, input.id);
+        if (input.name) {
+            stockLocation.name = input.name;
+        }
+        if (input.description) {
+            stockLocation.description = input.description;
+        }
+        return this.connection.getRepository(ctx, StockLocation).save(stockLocation);
+    }
+
+    async delete(ctx: RequestContext, input: DeleteStockLocationInput): Promise<StockLocation> {
+        const stockLocation = await this.connection.getEntityOrThrow(ctx, StockLocation, input.id);
+        await this.connection.getRepository(ctx, StockLocation).remove(stockLocation);
+        return stockLocation;
+    }
+
     getAllStockLocations(ctx: RequestContext) {
         return this.requestContextCache.get(ctx, `StockLocationService.getAllStockLocations`, () =>
             this.connection.getRepository(ctx, StockLocation).find(),

+ 77 - 0
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -817,6 +817,12 @@ export type CreateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    name: Scalars['String'];
+};
+
 export type CreateTagInput = {
     value: Scalars['String'];
 };
@@ -1392,6 +1398,11 @@ export type DeleteAssetsInput = {
     force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+    id: Scalars['ID'];
+    transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
     message?: Maybe<Scalars['String']>;
     result: DeletionResult;
@@ -2447,6 +2458,7 @@ export type Mutation = {
     createSeller: Seller;
     /** Create a new ShippingMethod */
     createShippingMethod: ShippingMethod;
+    createStockLocation: StockLocation;
     /** Create a new Tag */
     createTag: Tag;
     /** Create a new TaxCategory */
@@ -2504,6 +2516,7 @@ export type Mutation = {
     deleteSeller: DeletionResponse;
     /** Delete a ShippingMethod */
     deleteShippingMethod: DeletionResponse;
+    deleteStockLocation: DeletionResponse;
     /** Delete an existing Tag */
     deleteTag: DeletionResponse;
     /** Deletes a TaxCategory */
@@ -2608,6 +2621,7 @@ export type Mutation = {
     updateSeller: Seller;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
+    updateStockLocation: StockLocation;
     /** Update an existing Tag */
     updateTag: Tag;
     /** Update an existing TaxCategory */
@@ -2788,6 +2802,10 @@ export type MutationCreateShippingMethodArgs = {
     input: CreateShippingMethodInput;
 };
 
+export type MutationCreateStockLocationArgs = {
+    input: CreateStockLocationInput;
+};
+
 export type MutationCreateTagArgs = {
     input: CreateTagInput;
 };
@@ -2912,6 +2930,10 @@ export type MutationDeleteShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationDeleteStockLocationArgs = {
+    input: DeleteStockLocationInput;
+};
+
 export type MutationDeleteTagArgs = {
     id: Scalars['ID'];
 };
@@ -3153,6 +3175,10 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
+export type MutationUpdateStockLocationArgs = {
+    input: UpdateStockLocationInput;
+};
+
 export type MutationUpdateTagArgs = {
     input: UpdateTagInput;
 };
@@ -4266,6 +4292,8 @@ export type Query = {
     shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingMethods: ShippingMethodList;
+    stockLocation?: Maybe<StockLocation>;
+    stockLocations: StockLocationList;
     tag: Tag;
     tags: TagList;
     taxCategories: Array<TaxCategory>;
@@ -4446,6 +4474,14 @@ export type QueryShippingMethodsArgs = {
     options?: InputMaybe<ShippingMethodListOptions>;
 };
 
+export type QueryStockLocationArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryStockLocationsArgs = {
+    options?: InputMaybe<StockLocationListOptions>;
+};
+
 export type QueryTagArgs = {
     id: Scalars['ID'];
 };
@@ -4944,6 +4980,40 @@ export type StockLocation = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+    createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
+    id?: InputMaybe<IdOperators>;
+    name?: InputMaybe<StringOperators>;
+    updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+    items: Array<StockLocation>;
+    totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+    /** Allows the results to be filtered */
+    filter?: InputMaybe<StockLocationFilterParameter>;
+    /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+    filterOperator?: InputMaybe<LogicalOperator>;
+    /** Skips the first n results, for use in pagination */
+    skip?: InputMaybe<Scalars['Int']>;
+    /** Specifies which properties to sort the results by */
+    sort?: InputMaybe<StockLocationSortParameter>;
+    /** Takes n results, for use in pagination */
+    take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+    createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
+    id?: InputMaybe<SortOrder>;
+    name?: InputMaybe<SortOrder>;
+    updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
     createdAt: Scalars['DateTime'];
     id: Scalars['ID'];
@@ -5440,6 +5510,13 @@ export type UpdateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id: Scalars['ID'];
+    name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
     id: Scalars['ID'];
     value?: InputMaybe<Scalars['String']>;

+ 77 - 0
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -817,6 +817,12 @@ export type CreateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type CreateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    name: Scalars['String'];
+};
+
 export type CreateTagInput = {
     value: Scalars['String'];
 };
@@ -1392,6 +1398,11 @@ export type DeleteAssetsInput = {
     force?: InputMaybe<Scalars['Boolean']>;
 };
 
+export type DeleteStockLocationInput = {
+    id: Scalars['ID'];
+    transferToLocationId?: InputMaybe<Scalars['ID']>;
+};
+
 export type DeletionResponse = {
     message?: Maybe<Scalars['String']>;
     result: DeletionResult;
@@ -2447,6 +2458,7 @@ export type Mutation = {
     createSeller: Seller;
     /** Create a new ShippingMethod */
     createShippingMethod: ShippingMethod;
+    createStockLocation: StockLocation;
     /** Create a new Tag */
     createTag: Tag;
     /** Create a new TaxCategory */
@@ -2504,6 +2516,7 @@ export type Mutation = {
     deleteSeller: DeletionResponse;
     /** Delete a ShippingMethod */
     deleteShippingMethod: DeletionResponse;
+    deleteStockLocation: DeletionResponse;
     /** Delete an existing Tag */
     deleteTag: DeletionResponse;
     /** Deletes a TaxCategory */
@@ -2608,6 +2621,7 @@ export type Mutation = {
     updateSeller: Seller;
     /** Update an existing ShippingMethod */
     updateShippingMethod: ShippingMethod;
+    updateStockLocation: StockLocation;
     /** Update an existing Tag */
     updateTag: Tag;
     /** Update an existing TaxCategory */
@@ -2788,6 +2802,10 @@ export type MutationCreateShippingMethodArgs = {
     input: CreateShippingMethodInput;
 };
 
+export type MutationCreateStockLocationArgs = {
+    input: CreateStockLocationInput;
+};
+
 export type MutationCreateTagArgs = {
     input: CreateTagInput;
 };
@@ -2912,6 +2930,10 @@ export type MutationDeleteShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type MutationDeleteStockLocationArgs = {
+    input: DeleteStockLocationInput;
+};
+
 export type MutationDeleteTagArgs = {
     id: Scalars['ID'];
 };
@@ -3153,6 +3175,10 @@ export type MutationUpdateShippingMethodArgs = {
     input: UpdateShippingMethodInput;
 };
 
+export type MutationUpdateStockLocationArgs = {
+    input: UpdateStockLocationInput;
+};
+
 export type MutationUpdateTagArgs = {
     input: UpdateTagInput;
 };
@@ -4266,6 +4292,8 @@ export type Query = {
     shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>;
     shippingMethod?: Maybe<ShippingMethod>;
     shippingMethods: ShippingMethodList;
+    stockLocation?: Maybe<StockLocation>;
+    stockLocations: StockLocationList;
     tag: Tag;
     tags: TagList;
     taxCategories: Array<TaxCategory>;
@@ -4446,6 +4474,14 @@ export type QueryShippingMethodsArgs = {
     options?: InputMaybe<ShippingMethodListOptions>;
 };
 
+export type QueryStockLocationArgs = {
+    id: Scalars['ID'];
+};
+
+export type QueryStockLocationsArgs = {
+    options?: InputMaybe<StockLocationListOptions>;
+};
+
 export type QueryTagArgs = {
     id: Scalars['ID'];
 };
@@ -4944,6 +4980,40 @@ export type StockLocation = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLocationFilterParameter = {
+    createdAt?: InputMaybe<DateOperators>;
+    description?: InputMaybe<StringOperators>;
+    id?: InputMaybe<IdOperators>;
+    name?: InputMaybe<StringOperators>;
+    updatedAt?: InputMaybe<DateOperators>;
+};
+
+export type StockLocationList = PaginatedList & {
+    items: Array<StockLocation>;
+    totalItems: Scalars['Int'];
+};
+
+export type StockLocationListOptions = {
+    /** Allows the results to be filtered */
+    filter?: InputMaybe<StockLocationFilterParameter>;
+    /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+    filterOperator?: InputMaybe<LogicalOperator>;
+    /** Skips the first n results, for use in pagination */
+    skip?: InputMaybe<Scalars['Int']>;
+    /** Specifies which properties to sort the results by */
+    sort?: InputMaybe<StockLocationSortParameter>;
+    /** Takes n results, for use in pagination */
+    take?: InputMaybe<Scalars['Int']>;
+};
+
+export type StockLocationSortParameter = {
+    createdAt?: InputMaybe<SortOrder>;
+    description?: InputMaybe<SortOrder>;
+    id?: InputMaybe<SortOrder>;
+    name?: InputMaybe<SortOrder>;
+    updatedAt?: InputMaybe<SortOrder>;
+};
+
 export type StockMovement = {
     createdAt: Scalars['DateTime'];
     id: Scalars['ID'];
@@ -5440,6 +5510,13 @@ export type UpdateShippingMethodInput = {
     translations: Array<ShippingMethodTranslationInput>;
 };
 
+export type UpdateStockLocationInput = {
+    customFields?: InputMaybe<Scalars['JSON']>;
+    description?: InputMaybe<Scalars['String']>;
+    id: Scalars['ID'];
+    name?: InputMaybe<Scalars['String']>;
+};
+
 export type UpdateTagInput = {
     id: Scalars['ID'];
     value?: InputMaybe<Scalars['String']>;

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
schema-admin.json


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff