Bladeren bron

feat(core): Implement data model & APIs for multi-location stock

Relates to #1545
Michael Bromley 3 jaren geleden
bovenliggende
commit
905c1dfb4f
40 gewijzigde bestanden met toevoegingen van 898 en 295 verwijderingen
  1. 15 29
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  2. 15 27
      packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts
  3. 15 29
      packages/common/src/generated-types.ts
  4. 15 27
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  5. 0 1
      packages/core/e2e/stock-control.e2e-spec.ts
  6. 22 0
      packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts
  7. 3 2
      packages/core/src/api/schema/admin-api/product-admin.type.graphql
  8. 7 0
      packages/core/src/api/schema/admin-api/product.api.graphql
  9. 9 0
      packages/core/src/api/schema/admin-api/stock-level.type.graphql
  10. 7 0
      packages/core/src/api/schema/admin-api/stock-location.type.graphql
  11. 62 0
      packages/core/src/config/catalog/default-stock-location-strategy.ts
  12. 106 0
      packages/core/src/config/catalog/stock-location-strategy.ts
  13. 1 0
      packages/core/src/config/custom-field/custom-field-types.ts
  14. 3 0
      packages/core/src/config/default-config.ts
  15. 6 1
      packages/core/src/config/fulfillment/default-fulfillment-process.ts
  16. 8 5
      packages/core/src/config/index.ts
  17. 3 0
      packages/core/src/config/order/default-order-process.ts
  18. 11 0
      packages/core/src/config/vendure-config.ts
  19. 1 3
      packages/core/src/data-import/providers/importer/fast-importer.service.ts
  20. 1 0
      packages/core/src/entity/custom-entity-fields.ts
  21. 4 0
      packages/core/src/entity/entities.ts
  22. 26 17
      packages/core/src/entity/index.ts
  23. 8 4
      packages/core/src/entity/product-variant/product-variant.entity.ts
  24. 2 0
      packages/core/src/entity/register-custom-entity-fields.ts
  25. 40 0
      packages/core/src/entity/stock-level/stock-level.entity.ts
  26. 33 0
      packages/core/src/entity/stock-location/stock-location.entity.ts
  27. 10 0
      packages/core/src/entity/stock-movement/stock-movement.entity.ts
  28. 6 4
      packages/core/src/service/index.ts
  29. 3 0
      packages/core/src/service/initializer.service.ts
  30. 4 0
      packages/core/src/service/service.module.ts
  31. 9 5
      packages/core/src/service/services/global-settings.service.ts
  32. 8 2
      packages/core/src/service/services/order.service.ts
  33. 18 21
      packages/core/src/service/services/product-variant.service.ts
  34. 122 0
      packages/core/src/service/services/stock-level.service.ts
  35. 119 0
      packages/core/src/service/services/stock-location.service.ts
  36. 145 63
      packages/core/src/service/services/stock-movement.service.ts
  37. 1 1
      packages/dev-server/dev-config.ts
  38. 15 27
      packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts
  39. 15 27
      packages/payments-plugin/e2e/graphql/generated-admin-types.ts
  40. 0 0
      schema-admin.json

+ 15 - 29
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -782,6 +782,7 @@ export type CreateProductVariantInput = {
   price?: InputMaybe<Scalars['Int']>;
   productId: Scalars['ID'];
   sku: Scalars['String'];
+  stockLevels?: InputMaybe<Array<StockLevelInput>>;
   stockOnHand?: InputMaybe<Scalars['Int']>;
   taxCategoryId?: InputMaybe<Scalars['ID']>;
   trackInventory?: InputMaybe<GlobalFlag>;
@@ -816,12 +817,8 @@ export type CreateRoleInput = {
   permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-  connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-  customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+  customFields?: InputMaybe<Scalars['JSON']>;
   name: Scalars['String'];
 };
 
@@ -1243,6 +1240,7 @@ export type CustomFields = {
   Promotion: Array<CustomFieldConfig>;
   Seller: Array<CustomFieldConfig>;
   ShippingMethod: Array<CustomFieldConfig>;
+  StockLocation: Array<CustomFieldConfig>;
   TaxCategory: Array<CustomFieldConfig>;
   TaxRate: Array<CustomFieldConfig>;
   User: Array<CustomFieldConfig>;
@@ -2579,7 +2577,6 @@ export type Mutation = {
   /** Move a Collection to a different parent or index */
   moveCollection: Collection;
   refundOrder: RefundOrderResult;
-  registerNewSeller?: Maybe<Channel>;
   reindex: Job;
   /** Removes Collections from the specified Channel */
   removeCollectionsFromChannel: Array<Collection>;
@@ -3101,11 +3098,6 @@ export type MutationRefundOrderArgs = {
 };
 
 
-export type MutationRegisterNewSellerArgs = {
-  input: RegisterSellerInput;
-};
-
-
 export type MutationRemoveCollectionsFromChannelArgs = {
   input: RemoveCollectionsFromChannelInput;
 };
@@ -4336,9 +4328,12 @@ export type ProductVariant = Node & {
   product: Product;
   productId: Scalars['ID'];
   sku: Scalars['String'];
+  /** @deprecated use stockLevels */
   stockAllocated: Scalars['Int'];
   stockLevel: Scalars['String'];
+  stockLevels: Array<StockLevel>;
   stockMovements: StockMovementList;
+  /** @deprecated use stockLevels */
   stockOnHand: Scalars['Int'];
   taxCategory: TaxCategory;
   taxRateApplied: TaxRate;
@@ -4884,11 +4879,6 @@ export type RefundStateTransitionError = ErrorResult & {
   transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-  administrator: CreateAdministratorInput;
-  shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
   __typename?: 'RelationCustomFieldConfig';
   description?: Maybe<Array<LocalizedString>>;
@@ -5084,19 +5074,13 @@ export type SearchResultSortParameter = {
 export type Seller = Node & {
   __typename?: 'Seller';
   createdAt: Scalars['DateTime'];
-  customFields?: Maybe<SellerCustomFields>;
+  customFields?: Maybe<Scalars['JSON']>;
   id: Scalars['ID'];
   name: Scalars['String'];
   updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-  __typename?: 'SellerCustomFields';
-  connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-  connectedAccountId?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
   id?: InputMaybe<IdOperators>;
   name?: InputMaybe<StringOperators>;
@@ -5123,7 +5107,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-  connectedAccountId?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
@@ -5289,9 +5272,15 @@ export type StockLevel = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+  stockLocationId: Scalars['ID'];
+  stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
   __typename?: 'StockLocation';
   createdAt: Scalars['DateTime'];
+  customFields?: Maybe<Scalars['JSON']>;
   description: Scalars['String'];
   id: Scalars['ID'];
   name: Scalars['String'];
@@ -5765,6 +5754,7 @@ export type UpdateProductVariantInput = {
   outOfStockThreshold?: InputMaybe<Scalars['Int']>;
   price?: InputMaybe<Scalars['Int']>;
   sku?: InputMaybe<Scalars['String']>;
+  stockLevels?: InputMaybe<Array<StockLevelInput>>;
   stockOnHand?: InputMaybe<Scalars['Int']>;
   taxCategoryId?: InputMaybe<Scalars['ID']>;
   trackInventory?: InputMaybe<GlobalFlag>;
@@ -5795,12 +5785,8 @@ export type UpdateRoleInput = {
   permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-  connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-  customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+  customFields?: InputMaybe<Scalars['JSON']>;
   id: Scalars['ID'];
   name?: InputMaybe<Scalars['String']>;
 };

+ 15 - 27
packages/asset-server-plugin/e2e/graphql/generated-e2e-asset-server-plugin-types.ts

@@ -768,6 +768,7 @@ export type CreateProductVariantInput = {
     price?: InputMaybe<Scalars['Int']>;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -802,12 +803,8 @@ export type CreateRoleInput = {
     permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-    customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     name: Scalars['String'];
 };
 
@@ -1227,6 +1224,7 @@ export type CustomFields = {
     Promotion: Array<CustomFieldConfig>;
     Seller: Array<CustomFieldConfig>;
     ShippingMethod: Array<CustomFieldConfig>;
+    StockLocation: Array<CustomFieldConfig>;
     TaxCategory: Array<CustomFieldConfig>;
     TaxRate: Array<CustomFieldConfig>;
     User: Array<CustomFieldConfig>;
@@ -2527,7 +2525,6 @@ export type Mutation = {
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
     refundOrder: RefundOrderResult;
-    registerNewSeller?: Maybe<Channel>;
     reindex: Job;
     /** Removes Collections from the specified Channel */
     removeCollectionsFromChannel: Array<Collection>;
@@ -2957,10 +2954,6 @@ export type MutationRefundOrderArgs = {
     input: RefundOrderInput;
 };
 
-export type MutationRegisterNewSellerArgs = {
-    input: RegisterSellerInput;
-};
-
 export type MutationRemoveCollectionsFromChannelArgs = {
     input: RemoveCollectionsFromChannelInput;
 };
@@ -4058,9 +4051,12 @@ export type ProductVariant = Node & {
     product: Product;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    /** @deprecated use stockLevels */
     stockAllocated: Scalars['Int'];
     stockLevel: Scalars['String'];
+    stockLevels: Array<StockLevel>;
     stockMovements: StockMovementList;
+    /** @deprecated use stockLevels */
     stockOnHand: Scalars['Int'];
     taxCategory: TaxCategory;
     taxRateApplied: TaxRate;
@@ -4551,11 +4547,6 @@ export type RefundStateTransitionError = ErrorResult & {
     transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-    administrator: CreateAdministratorInput;
-    shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
     description?: Maybe<Array<LocalizedString>>;
     entity: Scalars['String'];
@@ -4743,18 +4734,13 @@ export type SearchResultSortParameter = {
 
 export type Seller = Node & {
     createdAt: Scalars['DateTime'];
-    customFields?: Maybe<SellerCustomFields>;
+    customFields?: Maybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-    connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-    connectedAccountId?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     name?: InputMaybe<StringOperators>;
@@ -4780,7 +4766,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-    connectedAccountId?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4945,8 +4930,14 @@ export type StockLevel = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+    stockLocationId: Scalars['ID'];
+    stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
     createdAt: Scalars['DateTime'];
+    customFields?: Maybe<Scalars['JSON']>;
     description: Scalars['String'];
     id: Scalars['ID'];
     name: Scalars['String'];
@@ -5402,6 +5393,7 @@ export type UpdateProductVariantInput = {
     outOfStockThreshold?: InputMaybe<Scalars['Int']>;
     price?: InputMaybe<Scalars['Int']>;
     sku?: InputMaybe<Scalars['String']>;
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -5432,12 +5424,8 @@ export type UpdateRoleInput = {
     permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-    customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name?: InputMaybe<Scalars['String']>;
 };

+ 15 - 29
packages/common/src/generated-types.ts

@@ -782,6 +782,7 @@ export type CreateProductVariantInput = {
   price?: InputMaybe<Scalars['Int']>;
   productId: Scalars['ID'];
   sku: Scalars['String'];
+  stockLevels?: InputMaybe<Array<StockLevelInput>>;
   stockOnHand?: InputMaybe<Scalars['Int']>;
   taxCategoryId?: InputMaybe<Scalars['ID']>;
   trackInventory?: InputMaybe<GlobalFlag>;
@@ -816,12 +817,8 @@ export type CreateRoleInput = {
   permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-  connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-  customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+  customFields?: InputMaybe<Scalars['JSON']>;
   name: Scalars['String'];
 };
 
@@ -1236,6 +1233,7 @@ export type CustomFields = {
   Promotion: Array<CustomFieldConfig>;
   Seller: Array<CustomFieldConfig>;
   ShippingMethod: Array<CustomFieldConfig>;
+  StockLocation: Array<CustomFieldConfig>;
   TaxCategory: Array<CustomFieldConfig>;
   TaxRate: Array<CustomFieldConfig>;
   User: Array<CustomFieldConfig>;
@@ -2572,7 +2570,6 @@ export type Mutation = {
   /** Move a Collection to a different parent or index */
   moveCollection: Collection;
   refundOrder: RefundOrderResult;
-  registerNewSeller?: Maybe<Channel>;
   reindex: Job;
   /** Removes Collections from the specified Channel */
   removeCollectionsFromChannel: Array<Collection>;
@@ -3083,11 +3080,6 @@ export type MutationRefundOrderArgs = {
 };
 
 
-export type MutationRegisterNewSellerArgs = {
-  input: RegisterSellerInput;
-};
-
-
 export type MutationRemoveCollectionsFromChannelArgs = {
   input: RemoveCollectionsFromChannelInput;
 };
@@ -4272,9 +4264,12 @@ export type ProductVariant = Node & {
   product: Product;
   productId: Scalars['ID'];
   sku: Scalars['String'];
+  /** @deprecated use stockLevels */
   stockAllocated: Scalars['Int'];
   stockLevel: Scalars['String'];
+  stockLevels: Array<StockLevel>;
   stockMovements: StockMovementList;
+  /** @deprecated use stockLevels */
   stockOnHand: Scalars['Int'];
   taxCategory: TaxCategory;
   taxRateApplied: TaxRate;
@@ -4817,11 +4812,6 @@ export type RefundStateTransitionError = ErrorResult & {
   transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-  administrator: CreateAdministratorInput;
-  shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
   __typename?: 'RelationCustomFieldConfig';
   description?: Maybe<Array<LocalizedString>>;
@@ -5017,19 +5007,13 @@ export type SearchResultSortParameter = {
 export type Seller = Node & {
   __typename?: 'Seller';
   createdAt: Scalars['DateTime'];
-  customFields?: Maybe<SellerCustomFields>;
+  customFields?: Maybe<Scalars['JSON']>;
   id: Scalars['ID'];
   name: Scalars['String'];
   updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-  __typename?: 'SellerCustomFields';
-  connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-  connectedAccountId?: InputMaybe<StringOperators>;
   createdAt?: InputMaybe<DateOperators>;
   id?: InputMaybe<IdOperators>;
   name?: InputMaybe<StringOperators>;
@@ -5056,7 +5040,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-  connectedAccountId?: InputMaybe<SortOrder>;
   createdAt?: InputMaybe<SortOrder>;
   id?: InputMaybe<SortOrder>;
   name?: InputMaybe<SortOrder>;
@@ -5222,9 +5205,15 @@ export type StockLevel = Node & {
   updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+  stockLocationId: Scalars['ID'];
+  stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
   __typename?: 'StockLocation';
   createdAt: Scalars['DateTime'];
+  customFields?: Maybe<Scalars['JSON']>;
   description: Scalars['String'];
   id: Scalars['ID'];
   name: Scalars['String'];
@@ -5689,6 +5678,7 @@ export type UpdateProductVariantInput = {
   outOfStockThreshold?: InputMaybe<Scalars['Int']>;
   price?: InputMaybe<Scalars['Int']>;
   sku?: InputMaybe<Scalars['String']>;
+  stockLevels?: InputMaybe<Array<StockLevelInput>>;
   stockOnHand?: InputMaybe<Scalars['Int']>;
   taxCategoryId?: InputMaybe<Scalars['ID']>;
   trackInventory?: InputMaybe<GlobalFlag>;
@@ -5719,12 +5709,8 @@ export type UpdateRoleInput = {
   permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-  connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-  customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+  customFields?: InputMaybe<Scalars['JSON']>;
   id: Scalars['ID'];
   name?: InputMaybe<Scalars['String']>;
 };

+ 15 - 27
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -768,6 +768,7 @@ export type CreateProductVariantInput = {
     price?: InputMaybe<Scalars['Int']>;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -802,12 +803,8 @@ export type CreateRoleInput = {
     permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-    customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     name: Scalars['String'];
 };
 
@@ -1227,6 +1224,7 @@ export type CustomFields = {
     Promotion: Array<CustomFieldConfig>;
     Seller: Array<CustomFieldConfig>;
     ShippingMethod: Array<CustomFieldConfig>;
+    StockLocation: Array<CustomFieldConfig>;
     TaxCategory: Array<CustomFieldConfig>;
     TaxRate: Array<CustomFieldConfig>;
     User: Array<CustomFieldConfig>;
@@ -2527,7 +2525,6 @@ export type Mutation = {
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
     refundOrder: RefundOrderResult;
-    registerNewSeller?: Maybe<Channel>;
     reindex: Job;
     /** Removes Collections from the specified Channel */
     removeCollectionsFromChannel: Array<Collection>;
@@ -2957,10 +2954,6 @@ export type MutationRefundOrderArgs = {
     input: RefundOrderInput;
 };
 
-export type MutationRegisterNewSellerArgs = {
-    input: RegisterSellerInput;
-};
-
 export type MutationRemoveCollectionsFromChannelArgs = {
     input: RemoveCollectionsFromChannelInput;
 };
@@ -4058,9 +4051,12 @@ export type ProductVariant = Node & {
     product: Product;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    /** @deprecated use stockLevels */
     stockAllocated: Scalars['Int'];
     stockLevel: Scalars['String'];
+    stockLevels: Array<StockLevel>;
     stockMovements: StockMovementList;
+    /** @deprecated use stockLevels */
     stockOnHand: Scalars['Int'];
     taxCategory: TaxCategory;
     taxRateApplied: TaxRate;
@@ -4551,11 +4547,6 @@ export type RefundStateTransitionError = ErrorResult & {
     transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-    administrator: CreateAdministratorInput;
-    shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
     description?: Maybe<Array<LocalizedString>>;
     entity: Scalars['String'];
@@ -4743,18 +4734,13 @@ export type SearchResultSortParameter = {
 
 export type Seller = Node & {
     createdAt: Scalars['DateTime'];
-    customFields?: Maybe<SellerCustomFields>;
+    customFields?: Maybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-    connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-    connectedAccountId?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     name?: InputMaybe<StringOperators>;
@@ -4780,7 +4766,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-    connectedAccountId?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4945,8 +4930,14 @@ export type StockLevel = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+    stockLocationId: Scalars['ID'];
+    stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
     createdAt: Scalars['DateTime'];
+    customFields?: Maybe<Scalars['JSON']>;
     description: Scalars['String'];
     id: Scalars['ID'];
     name: Scalars['String'];
@@ -5402,6 +5393,7 @@ export type UpdateProductVariantInput = {
     outOfStockThreshold?: InputMaybe<Scalars['Int']>;
     price?: InputMaybe<Scalars['Int']>;
     sku?: InputMaybe<Scalars['String']>;
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -5432,12 +5424,8 @@ export type UpdateRoleInput = {
     permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-    customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name?: InputMaybe<Scalars['String']>;
 };

+ 0 - 1
packages/core/e2e/stock-control.e2e-spec.ts

@@ -413,7 +413,6 @@ describe('Stock control', () => {
 
             const product = await getProductWithStockMovement('T_2');
             const [variant1, variant2, variant3] = product!.variants;
-
             expect(variant1.stockMovements.totalItems).toBe(3);
             expect(variant1.stockMovements.items[2].type).toBe(StockMovementType.SALE);
             expect(variant1.stockMovements.items[2].quantity).toBe(-2);

+ 22 - 0
packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts

@@ -10,6 +10,7 @@ import { Asset, Channel, FacetValue, Product, ProductOption, TaxRate } from '../
 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';
+import { StockLevelService } from '../../../service/index';
 import { AssetService } from '../../../service/services/asset.service';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
 import { StockMovementService } from '../../../service/services/stock-movement.service';
@@ -152,6 +153,7 @@ export class ProductVariantAdminEntityResolver {
     constructor(
         private productVariantService: ProductVariantService,
         private stockMovementService: StockMovementService,
+        private stockLevelService: StockLevelService,
     ) {}
 
     @ResolveField()
@@ -167,6 +169,26 @@ export class ProductVariantAdminEntityResolver {
         );
     }
 
+    @ResolveField()
+    async stockOnHand(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+        @Args() args: { options: StockMovementListOptions },
+    ): Promise<number> {
+        const { stockOnHand } = await this.stockLevelService.getAvailableStock(ctx, productVariant.id);
+        return stockOnHand;
+    }
+
+    @ResolveField()
+    async stockAllocated(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+        @Args() args: { options: StockMovementListOptions },
+    ): Promise<number> {
+        const { stockAllocated } = await this.stockLevelService.getAvailableStock(ctx, productVariant.id);
+        return stockAllocated;
+    }
+
     @ResolveField()
     async channels(@Ctx() ctx: RequestContext, @Parent() productVariant: ProductVariant): Promise<Channel[]> {
         const isDefaultChannel = ctx.channel.code === DEFAULT_CHANNEL_CODE;

+ 3 - 2
packages/core/src/api/schema/admin-api/product-admin.type.graphql

@@ -6,10 +6,11 @@ type Product implements Node {
 type ProductVariant implements Node {
     enabled: Boolean!
     trackInventory: GlobalFlag!
-    stockOnHand: Int!
-    stockAllocated: Int!
+    stockOnHand: Int! @deprecated(reason: "use stockLevels")
+    stockAllocated: Int! @deprecated(reason: "use stockLevels")
     outOfStockThreshold: Int!
     useGlobalOutOfStockThreshold: Boolean!
+    stockLevels: [StockLevel!]!
     stockMovements(options: StockMovementListOptions): StockMovementList!
     channels: [Channel!]!
 }

+ 7 - 0
packages/core/src/api/schema/admin-api/product.api.graphql

@@ -111,6 +111,11 @@ input CreateProductVariantOptionInput {
     translations: [ProductOptionTranslationInput!]!
 }
 
+input StockLevelInput {
+    stockLocationId: ID!
+    stockOnHand: Int!
+}
+
 input CreateProductVariantInput {
     productId: ID!
     translations: [ProductVariantTranslationInput!]!
@@ -122,6 +127,7 @@ input CreateProductVariantInput {
     featuredAssetId: ID
     assetIds: [ID!]
     stockOnHand: Int
+    stockLevels: [StockLevelInput!]
     outOfStockThreshold: Int
     useGlobalOutOfStockThreshold: Boolean
     trackInventory: GlobalFlag
@@ -138,6 +144,7 @@ input UpdateProductVariantInput {
     featuredAssetId: ID
     assetIds: [ID!]
     stockOnHand: Int
+    stockLevels: [StockLevelInput!]
     outOfStockThreshold: Int
     useGlobalOutOfStockThreshold: Boolean
     trackInventory: GlobalFlag

+ 9 - 0
packages/core/src/api/schema/admin-api/stock-level.type.graphql

@@ -0,0 +1,9 @@
+type StockLevel implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    stockLocationId: ID!
+    stockOnHand: Int!
+    stockAllocated: Int!
+    stockLocation: StockLocation!
+}

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

@@ -0,0 +1,7 @@
+type StockLocation implements Node {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    name: String!
+    description: String!
+}

+ 62 - 0
packages/core/src/config/catalog/default-stock-location-strategy.ts

@@ -0,0 +1,62 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/index';
+import { OrderLine, StockLevel, StockLocation } from '../../entity/index';
+
+import { AvailableStock, LocationWithQuantity, StockLocationStrategy } from './stock-location-strategy';
+
+/**
+ * @description
+ * The DefaultStockLocationStrategy is the default implementation of the {@link StockLocationStrategy}.
+ * It assumes only a single StockLocation and that all stock is allocated from that location.
+ *
+ * @docsCategory catalog
+ * @since 2.0.0
+ */
+export class DefaultStockLocationStrategy implements StockLocationStrategy {
+    getAvailableStock(ctx: RequestContext, productVariantId: ID, stockLevels: StockLevel[]): AvailableStock {
+        let stockOnHand = 0;
+        let stockAllocated = 0;
+        for (const stockLevel of stockLevels) {
+            stockOnHand += stockLevel.stockOnHand;
+            stockAllocated += stockLevel.stockAllocated;
+        }
+        return { stockOnHand, stockAllocated };
+    }
+
+    forAllocation(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ) {
+        return [{ location: stockLocations[0], quantity }];
+    }
+
+    forRelease(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
+        return [{ location: stockLocations[0], quantity }];
+    }
+
+    forSale(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
+        return [{ location: stockLocations[0], quantity }];
+    }
+
+    forCancellation(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]> {
+        return [{ location: stockLocations[0], quantity }];
+    }
+}

+ 106 - 0
packages/core/src/config/catalog/stock-location-strategy.ts

@@ -0,0 +1,106 @@
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/common/request-context';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+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';
+
+/**
+ * @description
+ * The overall available stock for a ProductVariant as determined by the logic of the
+ * {@link StockLocationStrategy}'s `getAvailableStock` method.
+ *
+ * @docsCategory catalog
+ * @since 2.0.0
+ * @docsPage StockLocationStrategy
+ */
+export interface AvailableStock {
+    stockOnHand: number;
+    stockAllocated: number;
+}
+
+/**
+ * @description
+ * Returned by the `StockLocationStrategy` methods to indicate how much stock from each
+ * location should be used in the allocation/sale/release/cancellation of an OrderLine.
+ *
+ * @docsCategory catalog
+ * @since 2.0.0
+ * @docsPage StockLocationStrategy
+ */
+export interface LocationWithQuantity {
+    location: StockLocation;
+    quantity: number;
+}
+
+/**
+ * @description
+ * The StockLocationStrategy is responsible for determining which {@link StockLocation}s
+ * should be used to fulfill an {@link OrderLine} and how much stock should be allocated
+ * from each location. It is also used to determine the available stock for a given
+ * {@link ProductVariant}.
+ *
+ * @docsCategory catalog
+ * @since 2.0.0
+ */
+export interface StockLocationStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * Returns the available stock for the given ProductVariant, taking into account
+     * the stock levels at each StockLocation.
+     */
+    getAvailableStock(
+        ctx: RequestContext,
+        productVariantId: ID,
+        stockLevels: StockLevel[],
+    ): AvailableStock | Promise<AvailableStock>;
+
+    /**
+     * @description
+     * Determines which StockLocations should be used to when allocating stock when
+     * and Order is placed.
+     */
+    forAllocation(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]>;
+
+    /**
+     * @description
+     * Determines which StockLocations should be used to when releasing allocated
+     * stock when an OrderLine is cancelled before being fulfilled.
+     */
+    forRelease(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]>;
+
+    /**
+     * @description
+     * Determines which StockLocations should be used to when creating a Sale
+     * and reducing the stockOnHand upon fulfillment.
+     */
+    forSale(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]>;
+
+    /**
+     * @description
+     * Determines which StockLocations should be used to when creating a Cancellation
+     * of an OrderLine which has already been fulfilled.
+     */
+    forCancellation(
+        ctx: RequestContext,
+        stockLocations: StockLocation[],
+        orderLine: OrderLine,
+        quantity: number,
+    ): LocationWithQuantity[] | Promise<LocationWithQuantity[]>;
+}

+ 1 - 0
packages/core/src/config/custom-field/custom-field-types.ts

@@ -219,6 +219,7 @@ export interface CustomFields {
     Promotion?: CustomFieldConfig[];
     Seller?: CustomFieldConfig[];
     ShippingMethod?: CustomFieldConfig[];
+    StockLocation?: CustomFieldConfig[];
     TaxCategory?: CustomFieldConfig[];
     TaxRate?: CustomFieldConfig[];
     User?: CustomFieldConfig[];

+ 3 - 0
packages/core/src/config/default-config.ts

@@ -19,6 +19,7 @@ import { NativeAuthenticationStrategy } from './auth/native-authentication-strat
 import { defaultCollectionFilters } from './catalog/default-collection-filters';
 import { DefaultProductVariantPriceCalculationStrategy } from './catalog/default-product-variant-price-calculation-strategy';
 import { DefaultStockDisplayStrategy } from './catalog/default-stock-display-strategy';
+import { DefaultStockLocationStrategy } from './catalog/default-stock-location-strategy';
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
 import { defaultFulfillmentProcess } from './fulfillment/default-fulfillment-process';
 import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handler';
@@ -103,6 +104,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         collectionFilters: defaultCollectionFilters,
         productVariantPriceCalculationStrategy: new DefaultProductVariantPriceCalculationStrategy(),
         stockDisplayStrategy: new DefaultStockDisplayStrategy(),
+        stockLocationStrategy: new DefaultStockLocationStrategy(),
     },
     entityIdStrategy: new AutoIncrementIdStrategy(),
     assetOptions: {
@@ -193,6 +195,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         Promotion: [],
         Seller: [],
         ShippingMethod: [],
+        StockLocation: [],
         TaxCategory: [],
         TaxRate: [],
         User: [],

+ 6 - 1
packages/core/src/config/fulfillment/default-fulfillment-process.ts

@@ -22,6 +22,7 @@ let configService: import('../config.service').ConfigService;
 let orderService: import('../../service/index').OrderService;
 let historyService: import('../../service/index').HistoryService;
 let stockMovementService: import('../../service/index').StockMovementService;
+let stockLevelService: import('../../service/index').StockLevelService;
 
 /**
  * @description
@@ -35,6 +36,8 @@ let stockMovementService: import('../../service/index').StockMovementService;
  *   {@link Sale} stock movements are created.
  *
  * @docsCategory fulfillment
+ * @docsPage FulfillmentProcess
+ * @since 2.0.0
  */
 export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
     transitions: {
@@ -64,11 +67,13 @@ export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
         const HistoryService = await import('../../service/index').then(m => m.HistoryService);
         const OrderService = await import('../../service/index').then(m => m.OrderService);
         const StockMovementService = await import('../../service/index').then(m => m.StockMovementService);
+        const StockLevelService = await import('../../service/index').then(m => m.StockLevelService);
         connection = injector.get(TransactionalConnection);
         configService = injector.get(ConfigService);
         orderService = injector.get(OrderService);
         historyService = injector.get(HistoryService);
         stockMovementService = injector.get(StockMovementService);
+        stockLevelService = injector.get(StockLevelService);
     },
     async onTransitionStart(fromState, toState, data) {
         const { fulfillmentHandlers } = configService.shippingOptions;
@@ -90,7 +95,7 @@ export const defaultFulfillmentProcess: FulfillmentProcess<FulfillmentState> = {
             await stockMovementService.createAllocationsForOrderLines(ctx, orderLineInput);
         }
         if (fromState === 'Created' && toState === 'Pending') {
-            await stockMovementService.createSalesForOrder(ctx, fulfillment.orderItems);
+            await stockMovementService.createSalesForOrder(ctx, fulfillment.lines);
         }
         const historyEntryPromises = orders.map(order =>
             historyService.createHistoryEntryForOrder({

+ 8 - 5
packages/core/src/config/index.ts

@@ -11,7 +11,10 @@ export * from './auth/password-hashing-strategy';
 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/product-variant-price-calculation-strategy';
+export * from './catalog/stock-display-strategy';
+export * from './catalog/stock-location-strategy';
 export * from './config.module';
 export * from './config.service';
 export * from './custom-field/custom-field-types';
@@ -19,11 +22,11 @@ export * from './default-config';
 export * from './entity-id-strategy/auto-increment-id-strategy';
 export * from './entity-id-strategy/entity-id-strategy';
 export * from './entity-id-strategy/uuid-id-strategy';
-export * from './entity-metadata/entity-metadata-modifier';
 export * from './entity-metadata/add-foreign-key-indices';
+export * from './entity-metadata/entity-metadata-modifier';
 export * from './fulfillment/default-fulfillment-process';
-export * from './fulfillment/fulfillment-process';
 export * from './fulfillment/fulfillment-handler';
+export * from './fulfillment/fulfillment-process';
 export * from './fulfillment/manual-fulfillment-handler';
 export * from './job-queue/inspectable-job-queue-strategy';
 export * from './job-queue/job-queue-strategy';
@@ -32,9 +35,8 @@ export * from './logger/noop-logger';
 export * from './logger/vendure-logger';
 export * from './merge-config';
 export * from './order/active-order-strategy';
-export * from './order/default-active-order-strategy';
 export * from './order/changed-price-handling-strategy';
-export * from './order/order-process';
+export * from './order/default-active-order-strategy';
 export * from './order/default-changed-price-handling-strategy';
 export * from './order/default-order-placed-strategy';
 export * from './order/default-order-process';
@@ -45,17 +47,18 @@ export * from './order/order-code-strategy';
 export * from './order/order-item-price-calculation-strategy';
 export * from './order/order-merge-strategy';
 export * from './order/order-placed-strategy';
+export * from './order/order-process';
 export * from './order/order-seller-strategy';
 export * from './order/stock-allocation-strategy';
 export * from './order/use-existing-strategy';
 export * from './order/use-guest-if-existing-empty-strategy';
 export * from './order/use-guest-strategy';
-export * from './payment/payment-process';
 export * from './payment/default-payment-process';
 export * from './payment/dummy-payment-method-handler';
 export * from './payment/example-payment-method-handler';
 export * from './payment/payment-method-eligibility-checker';
 export * from './payment/payment-method-handler';
+export * from './payment/payment-process';
 export * from './promotion';
 export * from './session-cache/in-memory-session-cache-strategy';
 export * from './session-cache/noop-session-cache-strategy';

+ 3 - 0
packages/core/src/config/order/default-order-process.ts

@@ -162,6 +162,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
     let configService: import('../config.service').ConfigService;
     let eventBus: import('../../event-bus/index').EventBus;
     let stockMovementService: import('../../service/index').StockMovementService;
+    let stockLevelService: import('../../service/index').StockLevelService;
     let historyService: import('../../service/index').HistoryService;
     let orderSplitter: import('../../service/index').OrderSplitter;
 
@@ -237,6 +238,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
             const StockMovementService = await import('../../service/index').then(
                 m => m.StockMovementService,
             );
+            const StockLevelService = await import('../../service/index').then(m => m.StockLevelService);
             const HistoryService = await import('../../service/index').then(m => m.HistoryService);
             const OrderSplitter = await import('../../service/index').then(m => m.OrderSplitter);
             const ProductVariantService = await import('../../service/index').then(
@@ -247,6 +249,7 @@ export function configureDefaultOrderProcess(options: DefaultOrderProcessOptions
             configService = injector.get(ConfigService);
             eventBus = injector.get(EventBus);
             stockMovementService = injector.get(StockMovementService);
+            stockLevelService = injector.get(StockLevelService);
             historyService = injector.get(HistoryService);
             orderSplitter = injector.get(OrderSplitter);
         },

+ 11 - 0
packages/core/src/config/vendure-config.ts

@@ -19,6 +19,7 @@ import { PasswordValidationStrategy } from './auth/password-validation-strategy'
 import { CollectionFilter } from './catalog/collection-filter';
 import { ProductVariantPriceCalculationStrategy } from './catalog/product-variant-price-calculation-strategy';
 import { StockDisplayStrategy } from './catalog/stock-display-strategy';
+import { StockLocationStrategy } from './catalog/stock-location-strategy';
 import { CustomFields } from './custom-field/custom-field-types';
 import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
 import { EntityMetadataModifier } from './entity-metadata/entity-metadata-modifier';
@@ -665,6 +666,16 @@ export interface CatalogOptions {
      * @default DefaultStockDisplayStrategy
      */
     stockDisplayStrategy?: StockDisplayStrategy;
+    /**
+     * @description
+     * Defines the strategy used to determine which StockLocation should be used when performing
+     * stock operations such as allocating and releasing stock as well as determining the
+     * amount of stock available for sale.
+     *
+     * @default DefaultStockLocationStrategy
+     * @since 2.0.0
+     */
+    stockLocationStrategy?: StockLocationStrategy;
 }
 
 /**

+ 1 - 3
packages/core/src/data-import/providers/importer/fast-importer.service.ts

@@ -23,9 +23,8 @@ import { ProductAsset } from '../../../entity/product/product-asset.entity';
 import { ProductTranslation } from '../../../entity/product/product-translation.entity';
 import { Product } from '../../../entity/product/product.entity';
 import { TranslatableSaver } from '../../../service/helpers/translatable-saver/translatable-saver';
-import { RequestContextService } from '../../../service/index';
+import { RequestContextService, StockMovementService } from '../../../service/index';
 import { ChannelService } from '../../../service/services/channel.service';
-import { StockMovementService } from '../../../service/services/stock-movement.service';
 
 /**
  * @description
@@ -187,7 +186,6 @@ export class FastImporterService {
             await this.stockMovementService.adjustProductVariantStock(
                 this.importCtx,
                 createdVariant.id,
-                0,
                 input.stockOnHand,
             );
         }

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

@@ -29,6 +29,7 @@ export class CustomPromotionFields {}
 export class CustomSellerFields {}
 export class CustomShippingMethodFields {}
 export class CustomShippingMethodFieldsTranslation {}
+export class CustomStockLocationFields {}
 export class CustomTaxCategoryFields {}
 export class CustomTaxRateFields {}
 export class CustomUserFields {}

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

@@ -51,6 +51,8 @@ import { Session } from './session/session.entity';
 import { ShippingLine } from './shipping-line/shipping-line.entity';
 import { ShippingMethodTranslation } from './shipping-method/shipping-method-translation.entity';
 import { ShippingMethod } from './shipping-method/shipping-method.entity';
+import { StockLevel } from './stock-level/stock-level.entity';
+import { StockLocation } from './stock-location/stock-location.entity';
 import { Allocation } from './stock-movement/allocation.entity';
 import { Cancellation } from './stock-movement/cancellation.entity';
 import { Release } from './stock-movement/release.entity';
@@ -125,6 +127,8 @@ export const coreEntitiesMap = {
     ShippingMethod,
     ShippingMethodTranslation,
     StockAdjustment,
+    StockLevel,
+    StockLocation,
     StockMovement,
     Surcharge,
     Tag,

+ 26 - 17
packages/core/src/entity/index.ts

@@ -1,4 +1,3 @@
-export * from './custom-entity-fields';
 export * from './address/address.entity';
 export * from './administrator/administrator.entity';
 export * from './asset/asset.entity';
@@ -7,17 +6,18 @@ export * from './authentication-method/external-authentication-method.entity';
 export * from './authentication-method/native-authentication-method.entity';
 export * from './base/base.entity';
 export * from './channel/channel.entity';
-export * from './collection/collection.entity';
 export * from './collection/collection-asset.entity';
 export * from './collection/collection-translation.entity';
-export * from './country/country.entity';
+export * from './collection/collection.entity';
 export * from './country/country-translation.entity';
-export * from './customer/customer.entity';
+export * from './country/country.entity';
+export * from './custom-entity-fields';
 export * from './customer-group/customer-group.entity';
-export * from './facet/facet.entity';
-export * from './facet/facet-translation.entity';
-export * from './facet-value/facet-value.entity';
+export * from './customer/customer.entity';
 export * from './facet-value/facet-value-translation.entity';
+export * from './facet-value/facet-value.entity';
+export * from './facet/facet-translation.entity';
+export * from './facet/facet.entity';
 export * from './fulfillment/fulfillment.entity';
 export * from './global-settings/global-settings.entity';
 export * from './order/order.entity';
@@ -28,26 +28,35 @@ export * from './order-line-reference/order-line-reference.entity';
 export * from './order-line-reference/refund-line.entity';
 export * from './payment/payment.entity';
 export * from './payment-method/payment-method.entity';
-export * from './product/product.entity';
-export * from './product/product-asset.entity';
-export * from './product/product-translation.entity';
-export * from './product-option/product-option.entity';
-export * from './product-option/product-option-translation.entity';
-export * from './product-option-group/product-option-group.entity';
+export * from './payment/payment.entity';
 export * from './product-option-group/product-option-group-translation.entity';
-export * from './product-variant/product-variant.entity';
+export * from './product-option-group/product-option-group.entity';
+export * from './product-option/product-option-translation.entity';
+export * from './product-option/product-option.entity';
 export * from './product-variant/product-variant-asset.entity';
-export * from './product-variant/product-variant-translation.entity';
 export * from './product-variant/product-variant-price.entity';
+export * from './product-variant/product-variant-translation.entity';
+export * from './product-variant/product-variant.entity';
+export * from './product/product-asset.entity';
+export * from './product/product-translation.entity';
+export * from './product/product.entity';
 export * from './promotion/promotion.entity';
 export * from './refund/refund.entity';
 export * from './role/role.entity';
-export * from './session/session.entity';
 export * from './session/anonymous-session.entity';
 export * from './session/authenticated-session.entity';
-export * from './surcharge/surcharge.entity';
+export * from './session/session.entity';
 export * from './shipping-line/shipping-line.entity';
 export * from './shipping-method/shipping-method.entity';
+export * from './stock-level/stock-level.entity';
+export * from './stock-location/stock-location.entity';
+export * from './stock-movement/allocation.entity';
+export * from './stock-movement/cancellation.entity';
+export * from './stock-movement/release.entity';
+export * from './stock-movement/sale.entity';
+export * from './stock-movement/stock-adjustment.entity';
+export * from './stock-movement/stock-movement.entity';
+export * from './surcharge/surcharge.entity';
 export * from './tag/tag.entity';
 export * from './tax-category/tax-category.entity';
 export * from './tax-rate/tax-rate.entity';

+ 8 - 4
packages/core/src/entity/product-variant/product-variant.entity.ts

@@ -15,6 +15,7 @@ import { EntityId } from '../entity-id.decorator';
 import { FacetValue } from '../facet-value/facet-value.entity';
 import { ProductOption } from '../product-option/product-option.entity';
 import { Product } from '../product/product.entity';
+import { StockLevel } from '../stock-level/stock-level.entity';
 import { StockMovement } from '../stock-movement/stock-movement.entity';
 import { TaxCategory } from '../tax-category/tax-category.entity';
 import { TaxRate } from '../tax-rate/tax-rate.entity';
@@ -122,11 +123,11 @@ export class ProductVariant
     @EntityId({ nullable: true })
     productId: ID;
 
-    @Column({ default: 0 })
-    stockOnHand: number;
+    // @Column({ default: 0 })
+    // stockOnHand: number;
 
-    @Column({ default: 0 })
-    stockAllocated: number;
+    // @Column({ default: 0 })
+    // stockAllocated: number;
 
     /**
      * @description
@@ -147,6 +148,9 @@ export class ProductVariant
     @Column({ type: 'varchar', default: GlobalFlag.INHERIT })
     trackInventory: GlobalFlag;
 
+    @OneToMany(type => StockLevel, stockLevel => stockLevel.productVariant)
+    stockLevels: StockLevel[];
+
     @OneToMany(type => StockMovement, stockMovement => stockMovement.productVariant)
     stockMovements: StockMovement[];
 

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

@@ -49,6 +49,7 @@ import {
     CustomSellerFields,
     CustomShippingMethodFields,
     CustomShippingMethodFieldsTranslation,
+    CustomStockLocationFields,
     CustomTaxCategoryFields,
     CustomTaxRateFields,
     CustomUserFields,
@@ -290,5 +291,6 @@ export function registerCustomEntityFields(config: VendureConfig) {
     registerCustomFieldsForEntity(config, 'Seller', CustomSellerFields);
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFields);
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFieldsTranslation, true);
+    registerCustomFieldsForEntity(config, 'StockLocation', CustomStockLocationFields);
     registerCustomFieldsForEntity(config, 'Zone', CustomZoneFields);
 }

+ 40 - 0
packages/core/src/entity/stock-level/stock-level.entity.ts

@@ -0,0 +1,40 @@
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
+import { Column, Entity, Index, ManyToOne } from 'typeorm';
+
+import { VendureEntity } from '../base/base.entity';
+import { EntityId } from '../entity-id.decorator';
+import { ProductVariant } from '../product-variant/product-variant.entity';
+import { StockLocation } from '../stock-location/stock-location.entity';
+
+/**
+ * @description
+ * A StockLevel represents the number of a particular {@link ProductVariant} which are available
+ * at a particular {@link StockLocation}.
+ *
+ * @docsCategory entities
+ */
+@Entity()
+@Index(['productVariantId', 'stockLocationId'], { unique: true })
+export class StockLevel extends VendureEntity {
+    constructor(input: DeepPartial<StockLevel>) {
+        super(input);
+    }
+
+    @ManyToOne(type => ProductVariant, productVariant => productVariant.stockLevels, { onDelete: 'CASCADE' })
+    productVariant: ProductVariant;
+
+    @EntityId()
+    productVariantId: ID;
+
+    @ManyToOne(type => StockLocation, { onDelete: 'CASCADE' })
+    stockLocation: StockLocation;
+
+    @EntityId()
+    stockLocationId: ID;
+
+    @Column()
+    stockOnHand: number;
+
+    @Column()
+    stockAllocated: number;
+}

+ 33 - 0
packages/core/src/entity/stock-location/stock-location.entity.ts

@@ -0,0 +1,33 @@
+import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
+
+import { ChannelAware } from '../../common/index';
+import { HasCustomFields } from '../../config/index';
+import { VendureEntity } from '../base/base.entity';
+import { Channel } from '../channel/channel.entity';
+import { CustomStockLocationFields } from '../custom-entity-fields';
+
+/**
+ * @description
+ * A StockLocation represents a physical location where stock is held. For example, a warehouse or a shop.
+ *
+ * @docsCategory entities
+ */
+@Entity()
+export class StockLocation extends VendureEntity implements HasCustomFields, ChannelAware {
+    constructor(input: DeepPartial<StockLocation>) {
+        super(input);
+    }
+    @Column()
+    name: string;
+
+    @Column()
+    description: string;
+
+    @Column(type => CustomStockLocationFields)
+    customFields: CustomStockLocationFields;
+
+    @ManyToMany(type => Channel)
+    @JoinTable()
+    channels: Channel[];
+}

+ 10 - 0
packages/core/src/entity/stock-movement/stock-movement.entity.ts

@@ -1,8 +1,11 @@
 import { StockMovementType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
 import { Column, Entity, Index, ManyToOne, TableInheritance } from 'typeorm';
 
 import { VendureEntity } from '../base/base.entity';
+import { EntityId } from '../entity-id.decorator';
 import { ProductVariant } from '../product-variant/product-variant.entity';
+import { StockLocation } from '../stock-location/stock-location.entity';
 
 /**
  * @description
@@ -23,6 +26,13 @@ export abstract class StockMovement extends VendureEntity {
     @ManyToOne(type => ProductVariant, variant => variant.stockMovements)
     productVariant: ProductVariant;
 
+    @Index()
+    @ManyToOne(type => StockLocation)
+    stockLocation: StockLocation;
+
+    @EntityId()
+    stockLocationId: ID;
+
     @Column()
     quantity: number;
 }

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

@@ -1,8 +1,8 @@
 export * from './helpers/active-order/active-order.service';
 export * from './helpers/config-arg/config-arg.service';
+export * from './helpers/custom-field-relation/custom-field-relation.service';
 export * from './helpers/entity-hydrator/entity-hydrator.service';
 export * from './helpers/external-authentication/external-authentication.service';
-export * from './helpers/custom-field-relation/custom-field-relation.service';
 export * from './helpers/fulfillment-state-machine/fulfillment-state';
 export * from './helpers/list-query-builder/list-query-builder';
 export * from './helpers/locale-string-hydrator/locale-string-hydrator';
@@ -19,9 +19,9 @@ export * from './helpers/refund-state-machine/refund-state';
 export * from './helpers/request-context/request-context.service';
 export * from './helpers/translatable-saver/translatable-saver';
 export * from './helpers/translator/translator.service';
+export * from './helpers/utils/order-utils';
 export * from './helpers/utils/patch-entity';
 export * from './helpers/utils/translate-entity';
-export * from './helpers/utils/order-utils';
 export * from './helpers/verification-token-generator/verification-token-generator';
 export * from './services/administrator.service';
 export * from './services/asset.service';
@@ -36,8 +36,8 @@ export * from './services/facet.service';
 export * from './services/fulfillment.service';
 export * from './services/global-settings.service';
 export * from './services/history.service';
-export * from './services/order.service';
 export * from './services/order-testing.service';
+export * from './services/order.service';
 export * from './services/payment-method.service';
 export * from './services/payment.service';
 export * from './services/product-option-group.service';
@@ -47,12 +47,14 @@ export * from './services/product.service';
 export * from './services/promotion.service';
 export * from './services/role.service';
 export * from './services/search.service';
+export * from './services/seller.service';
 export * from './services/session.service';
 export * from './services/shipping-method.service';
+export * from './services/stock-level.service';
+export * from './services/stock-location.service';
 export * from './services/stock-movement.service';
 export * from './services/tag.service';
 export * from './services/tax-category.service';
 export * from './services/tax-rate.service';
 export * from './services/user.service';
-export * from './services/seller.service';
 export * from './services/zone.service';

+ 3 - 0
packages/core/src/service/initializer.service.ts

@@ -12,6 +12,7 @@ import { GlobalSettingsService } from './services/global-settings.service';
 import { RoleService } from './services/role.service';
 import { SellerService } from './services/seller.service';
 import { ShippingMethodService } from './services/shipping-method.service';
+import { StockLocationService } from './services/stock-location.service';
 import { TaxRateService } from './services/tax-rate.service';
 import { ZoneService } from './services/zone.service';
 
@@ -32,6 +33,7 @@ export class InitializerService {
         private taxRateService: TaxRateService,
         private sellerService: SellerService,
         private eventBus: EventBus,
+        private stockLocationService: StockLocationService,
     ) {}
 
     async onModuleInit() {
@@ -50,6 +52,7 @@ export class InitializerService {
         await this.administratorService.initAdministrators();
         await this.shippingMethodService.initShippingMethods();
         await this.taxRateService.initTaxRates();
+        await this.stockLocationService.initStockLocations();
         this.eventBus.publish(new InitializerEvent());
     }
 

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

@@ -57,6 +57,8 @@ import { SearchService } from './services/search.service';
 import { SellerService } from './services/seller.service';
 import { SessionService } from './services/session.service';
 import { ShippingMethodService } from './services/shipping-method.service';
+import { StockLevelService } from './services/stock-level.service';
+import { StockLocationService } from './services/stock-location.service';
 import { StockMovementService } from './services/stock-movement.service';
 import { TagService } from './services/tag.service';
 import { TaxCategoryService } from './services/tax-category.service';
@@ -92,6 +94,8 @@ const services = [
     SellerService,
     SessionService,
     ShippingMethodService,
+    StockLevelService,
+    StockLocationService,
     StockMovementService,
     TagService,
     TaxCategoryService,

+ 9 - 5
packages/core/src/service/services/global-settings.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { UpdateGlobalSettingsInput } from '@vendure/common/lib/generated-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { RequestContextCacheService } from '../../cache/index';
 import { InternalServerError } from '../../common/error/errors';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
@@ -24,6 +25,7 @@ export class GlobalSettingsService {
         private configService: ConfigService,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
+        private requestCache: RequestContextCacheService,
     ) {}
 
     /**
@@ -56,11 +58,13 @@ export class GlobalSettingsService {
      * Returns the GlobalSettings entity.
      */
     async getSettings(ctx: RequestContext): Promise<GlobalSettings> {
-        const settings = await this.connection.getRepository(ctx, GlobalSettings).findOne({
-            order: {
-                createdAt: 'ASC',
-            },
-        });
+        const settings = await this.requestCache.get(ctx, 'globalSettings', () =>
+            this.connection.getRepository(ctx, GlobalSettings).findOne({
+                order: {
+                    createdAt: 'ASC',
+                },
+            }),
+        );
         if (!settings) {
             throw new InternalServerError(`error.global-settings-not-found`);
         }

+ 8 - 2
packages/core/src/service/services/order.service.ts

@@ -134,6 +134,7 @@ import { PaymentMethodService } from './payment-method.service';
 import { PaymentService } from './payment.service';
 import { ProductVariantService } from './product-variant.service';
 import { PromotionService } from './promotion.service';
+import { StockLevelService } from './stock-level.service';
 import { StockMovementService } from './stock-movement.service';
 
 /**
@@ -169,6 +170,7 @@ export class OrderService {
         private customFieldRelationService: CustomFieldRelationService,
         private requestCache: RequestContextCacheService,
         private translator: TranslatorService,
+        private stockLevelService: StockLevelService,
     ) {}
 
     /**
@@ -1208,7 +1210,6 @@ export class OrderService {
             .relation('fulfillments')
             .of(orders)
             .add(fulfillment);
-        await this.stockMovementService.createSalesForOrder(ctx, input.lines);
 
         for (const order of orders) {
             await this.historyService.createHistoryEntryForOrder({
@@ -1266,16 +1267,21 @@ export class OrderService {
         for (const line of lines) {
             // tslint:disable-next-line:no-non-null-assertion
             const lineInput = input.lines.find(l => idsAreEqual(l.orderLineId, line.id))!;
+
             const fulfillableStockLevel = await this.productVariantService.getFulfillableStockLevel(
                 ctx,
                 line.productVariant,
             );
             if (fulfillableStockLevel < lineInput.quantity) {
+                const { stockOnHand } = await this.stockLevelService.getAvailableStock(
+                    ctx,
+                    line.productVariant.id,
+                );
                 const productVariant = this.translator.translate(line.productVariant, ctx);
                 return new InsufficientStockOnHandError({
                     productVariantId: productVariant.id as string,
                     productVariantName: productVariant.name,
-                    stockOnHand: productVariant.stockOnHand,
+                    stockOnHand,
                 });
             }
         }

+ 18 - 21
packages/core/src/service/services/product-variant.service.ts

@@ -15,7 +15,7 @@ import { unique } from '@vendure/common/lib/unique';
 import { RequestContext } from '../../api/common/request-context';
 import { RelationPaths } from '../../api/decorators/relations.decorator';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
-import { ForbiddenError, InternalServerError, UserInputError } from '../../common/error/errors';
+import { ForbiddenError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { idsAreEqual } from '../../common/utils';
@@ -49,6 +49,7 @@ import { ChannelService } from './channel.service';
 import { FacetValueService } from './facet-value.service';
 import { GlobalSettingsService } from './global-settings.service';
 import { RoleService } from './role.service';
+import { StockLevelService } from './stock-level.service';
 import { StockMovementService } from './stock-movement.service';
 import { TaxCategoryService } from './tax-category.service';
 
@@ -71,6 +72,7 @@ export class ProductVariantService {
         private listQueryBuilder: ListQueryBuilder,
         private globalSettingsService: GlobalSettingsService,
         private stockMovementService: StockMovementService,
+        private stockLevelService: StockLevelService,
         private channelService: ChannelService,
         private roleService: RoleService,
         private customFieldRelationService: CustomFieldRelationService,
@@ -281,11 +283,7 @@ export class ProductVariantService {
      * as well as the local and global `outOfStockThreshold` settings.
      */
     async getSaleableStockLevel(ctx: RequestContext, variant: ProductVariant): Promise<number> {
-        const { outOfStockThreshold, trackInventory } = await this.requestCache.get(
-            ctx,
-            'globalSettings',
-            () => this.globalSettingsService.getSettings(ctx),
-        );
+        const { outOfStockThreshold, trackInventory } = await this.globalSettingsService.getSettings(ctx);
 
         const inventoryNotTracked =
             variant.trackInventory === GlobalFlag.FALSE ||
@@ -293,20 +291,19 @@ export class ProductVariantService {
         if (inventoryNotTracked) {
             return Number.MAX_SAFE_INTEGER;
         }
-
+        const { stockOnHand, stockAllocated } = await this.stockLevelService.getAvailableStock(
+            ctx,
+            variant.id,
+        );
         const effectiveOutOfStockThreshold = variant.useGlobalOutOfStockThreshold
             ? outOfStockThreshold
             : variant.outOfStockThreshold;
 
-        return variant.stockOnHand - variant.stockAllocated - effectiveOutOfStockThreshold;
+        return stockOnHand - stockAllocated - effectiveOutOfStockThreshold;
     }
 
     private async getOutOfStockThreshold(ctx: RequestContext, variant: ProductVariant): Promise<number> {
-        const { outOfStockThreshold, trackInventory } = await this.requestCache.get(
-            ctx,
-            'globalSettings',
-            () => this.globalSettingsService.getSettings(ctx),
-        );
+        const { outOfStockThreshold, trackInventory } = await this.globalSettingsService.getSettings(ctx);
 
         const inventoryNotTracked =
             variant.trackInventory === GlobalFlag.FALSE ||
@@ -342,8 +339,8 @@ export class ProductVariantService {
         if (inventoryNotTracked) {
             return Number.MAX_SAFE_INTEGER;
         }
-
-        return variant.stockOnHand;
+        const { stockOnHand } = await this.stockLevelService.getAvailableStock(ctx, variant.id);
+        return stockOnHand;
     }
 
     async create(
@@ -416,12 +413,12 @@ export class ProductVariantService {
         });
         await this.customFieldRelationService.updateRelations(ctx, ProductVariant, input, createdVariant);
         await this.assetService.updateEntityAssets(ctx, createdVariant, input);
-        if (input.stockOnHand != null && input.stockOnHand !== 0) {
+        if (input.stockOnHand != null || input.stockLevels) {
             await this.stockMovementService.adjustProductVariantStock(
                 ctx,
                 createdVariant.id,
-                0,
-                input.stockOnHand,
+                // tslint:disable-next-line:no-non-null-assertion
+                input.stockLevels || input.stockOnHand!,
             );
         }
 
@@ -475,12 +472,12 @@ export class ProductVariantService {
                         ...(await this.facetValueService.findByIds(ctx, input.facetValueIds)),
                     ];
                 }
-                if (input.stockOnHand != null) {
+                if (input.stockOnHand != null || input.stockLevels) {
                     await this.stockMovementService.adjustProductVariantStock(
                         ctx,
                         existingVariant.id,
-                        existingVariant.stockOnHand,
-                        input.stockOnHand,
+                        // tslint:disable-next-line:no-non-null-assertion
+                        input.stockLevels || input.stockOnHand!,
                     );
                 }
                 await this.assetService.updateFeaturedAsset(ctx, v, input);

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

@@ -0,0 +1,122 @@
+import { Injectable } from '@nestjs/common';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/index';
+import { AvailableStock, ConfigService } from '../../config/index';
+import { TransactionalConnection } from '../../connection/index';
+import { ProductVariant, StockLevel } from '../../entity/index';
+
+import { StockLocationService } from './stock-location.service';
+
+/**
+ * @description
+ * The StockLevelService is responsible for managing the stock levels of ProductVariants.
+ * Whenever you need to adjust the `stockOnHand` or `stockAllocated` for a ProductVariant,
+ * you should use this service.
+ *
+ * @docsCategory services
+ * @since 2.0.0
+ */
+@Injectable()
+export class StockLevelService {
+    constructor(
+        private connection: TransactionalConnection,
+        private stockLocationService: StockLocationService,
+        private configService: ConfigService,
+    ) {}
+
+    /**
+     * @description
+     * Returns the StockLevel for the given {@link ProductVariant} and {@link StockLocation}.
+     */
+    async getStockLevel(ctx: RequestContext, productVariantId: ID, stockLocationId: ID): Promise<StockLevel> {
+        const stockLevel = await this.connection.getRepository(ctx, StockLevel).findOne({
+            where: {
+                productVariantId,
+                stockLocationId,
+            },
+        });
+        if (stockLevel) {
+            return stockLevel;
+        }
+        return this.connection.getRepository(ctx, StockLevel).save(
+            new StockLevel({
+                productVariantId,
+                stockLocationId,
+                stockOnHand: 0,
+                stockAllocated: 0,
+            }),
+        );
+    }
+
+    /**
+     * @description
+     * Returns the available stock (on hand and allocated) for the given {@link ProductVariant}. This is determined
+     * by the configured {@link StockLocationStrategy}.
+     */
+    async getAvailableStock(ctx: RequestContext, productVariantId: ID): Promise<AvailableStock> {
+        const { stockLocationStrategy } = this.configService.catalogOptions;
+        const stockLevels = await this.connection.getRepository(ctx, StockLevel).find({
+            where: {
+                productVariantId,
+            },
+        });
+        return stockLocationStrategy.getAvailableStock(ctx, productVariantId, stockLevels);
+    }
+
+    /**
+     * @description
+     * Updates the `stockOnHand` for the given {@link ProductVariant} and {@link StockLocation}.
+     */
+    async updateStockOnHandForLocation(
+        ctx: RequestContext,
+        productVariantId: ID,
+        stockLocationId: ID,
+        change: number,
+    ) {
+        const stockLevel = await this.connection.getRepository(ctx, StockLevel).findOne({
+            where: {
+                productVariantId,
+                stockLocationId,
+            },
+        });
+        if (!stockLevel) {
+            await this.connection.getRepository(ctx, StockLevel).save(
+                new StockLevel({
+                    productVariantId,
+                    stockLocationId,
+                    stockOnHand: change,
+                    stockAllocated: 0,
+                }),
+            );
+        }
+        if (stockLevel) {
+            await this.connection
+                .getRepository(ctx, StockLevel)
+                .update(stockLevel.id, { stockOnHand: stockLevel.stockOnHand + change });
+        }
+    }
+
+    /**
+     * @description
+     * Updates the `stockAllocated` for the given {@link ProductVariant} and {@link StockLocation}.
+     */
+    async updateStockAllocatedForLocation(
+        ctx: RequestContext,
+        productVariantId: ID,
+        stockLocationId: ID,
+        change: number,
+    ) {
+        const stockLevel = await this.connection.getRepository(ctx, StockLevel).findOne({
+            where: {
+                productVariantId,
+                stockLocationId,
+            },
+        });
+        if (stockLevel) {
+            await this.connection
+                .getRepository(ctx, StockLevel)
+                .update(stockLevel.id, { stockAllocated: stockLevel.stockAllocated + change });
+        }
+    }
+}

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

@@ -0,0 +1,119 @@
+import { Injectable } from '@nestjs/common';
+import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
+
+import { RelationPaths, RequestContext } from '../../api/index';
+import { RequestContextCacheService } from '../../cache/index';
+import { ListQueryOptions } from '../../common/index';
+import { ConfigService } from '../../config/index';
+import { TransactionalConnection } from '../../connection/index';
+import { Order, OrderLine } from '../../entity/index';
+import { StockLocation } from '../../entity/stock-location/stock-location.entity';
+import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { RequestContextService } from '../helpers/request-context/request-context.service';
+
+import { ChannelService } from './channel.service';
+
+@Injectable()
+export class StockLocationService {
+    constructor(
+        private requestContextService: RequestContextService,
+        private connection: TransactionalConnection,
+        private channelService: ChannelService,
+        private listQueryBuilder: ListQueryBuilder,
+        private configService: ConfigService,
+        private requestContextCache: RequestContextCacheService,
+    ) {}
+
+    async initStockLocations() {
+        await this.ensureDefaultStockLocationExists();
+    }
+
+    findAll(
+        ctx: RequestContext,
+        options?: ListQueryOptions<StockLocation>,
+        relations?: RelationPaths<StockLocation>,
+    ): Promise<PaginatedList<StockLocation>> {
+        return this.listQueryBuilder
+            .build(StockLocation, options, {
+                relations,
+                ctx,
+            })
+            .getManyAndCount()
+            .then(([items, totalItems]) => ({
+                items,
+                totalItems,
+            }));
+    }
+
+    getAllStockLocations(ctx: RequestContext) {
+        return this.requestContextCache.get(ctx, `StockLocationService.getAllStockLocations`, () =>
+            this.connection.getRepository(ctx, StockLocation).find(),
+        );
+    }
+
+    defaultStockLocation(ctx: RequestContext) {
+        return this.connection
+            .getRepository(ctx, StockLocation)
+            .find({ order: { createdAt: 'ASC' } })
+            .then(items => items[0]);
+    }
+
+    async getAllocationLocations(ctx: RequestContext, orderLine: OrderLine, quantity: number) {
+        const { stockLocationStrategy } = this.configService.catalogOptions;
+        const stockLocations = await this.getAllStockLocations(ctx);
+        const allocationLocations = await stockLocationStrategy.forAllocation(
+            ctx,
+            stockLocations,
+            orderLine,
+            quantity,
+        );
+        return allocationLocations;
+    }
+
+    async getReleaseLocations(ctx: RequestContext, orderLine: OrderLine, quantity: number) {
+        const { stockLocationStrategy } = this.configService.catalogOptions;
+        const stockLocations = await this.getAllStockLocations(ctx);
+        const releaseLocations = await stockLocationStrategy.forRelease(
+            ctx,
+            stockLocations,
+            orderLine,
+            quantity,
+        );
+        return releaseLocations;
+    }
+
+    async getSaleLocations(ctx: RequestContext, orderLine: OrderLine, quantity: number) {
+        const { stockLocationStrategy } = this.configService.catalogOptions;
+        const stockLocations = await this.getAllStockLocations(ctx);
+        const saleLocations = await stockLocationStrategy.forSale(ctx, stockLocations, orderLine, quantity);
+        return saleLocations;
+    }
+
+    async getCancellationLocations(ctx: RequestContext, orderLine: OrderLine, quantity: number) {
+        const { stockLocationStrategy } = this.configService.catalogOptions;
+        const stockLocations = await this.getAllStockLocations(ctx);
+        const cancellationLocations = await stockLocationStrategy.forCancellation(
+            ctx,
+            stockLocations,
+            orderLine,
+            quantity,
+        );
+        return cancellationLocations;
+    }
+
+    private async ensureDefaultStockLocationExists() {
+        const ctx = await this.requestContextService.create({
+            apiType: 'admin',
+        });
+        const stockLocations = await this.connection.getRepository(ctx, StockLocation).find();
+        if (stockLocations.length === 0) {
+            const defaultStockLocation = await this.connection.getRepository(ctx, StockLocation).save(
+                new StockLocation({
+                    name: 'Default Stock Location',
+                    description: 'The default stock location',
+                }),
+            );
+            await this.channelService.assignToCurrentChannel(defaultStockLocation, ctx);
+        }
+    }
+}

+ 145 - 63
packages/core/src/service/services/stock-movement.service.ts

@@ -1,13 +1,20 @@
 import { Injectable } from '@nestjs/common';
-import { GlobalFlag, OrderLineInput, StockMovementListOptions } from '@vendure/common/lib/generated-types';
+import {
+    GlobalFlag,
+    OrderLineInput,
+    StockLevelInput,
+    StockMovementListOptions,
+} from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
 import { InternalServerError } from '../../common/error/errors';
 import { idsAreEqual } from '../../common/index';
+import { ConfigService } from '../../config/index';
 import { ShippingCalculator } from '../../config/shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { TransactionalConnection } from '../../connection/transactional-connection';
+import { StockLevel, StockLocation } from '../../entity/index';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
 import { Order } from '../../entity/order/order.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
@@ -23,6 +30,8 @@ import { StockMovementEvent } from '../../event-bus/events/stock-movement-event'
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 
 import { GlobalSettingsService } from './global-settings.service';
+import { StockLevelService } from './stock-level.service';
+import { StockLocationService } from './stock-location.service';
 
 /**
  * @description
@@ -40,7 +49,10 @@ export class StockMovementService {
         private connection: TransactionalConnection,
         private listQueryBuilder: ListQueryBuilder,
         private globalSettingsService: GlobalSettingsService,
+        private stockLevelService: StockLevelService,
         private eventBus: EventBus,
+        private stockLocationService: StockLocationService,
+        private configService: ConfigService,
     ) {}
 
     /**
@@ -73,22 +85,48 @@ export class StockMovementService {
     async adjustProductVariantStock(
         ctx: RequestContext,
         productVariantId: ID,
-        oldStockLevel: number,
-        newStockLevel: number,
-    ): Promise<StockAdjustment | undefined> {
-        if (oldStockLevel === newStockLevel) {
-            return;
+        stockOnHandNumberOrInput: number | StockLevelInput[],
+    ): Promise<StockAdjustment[]> {
+        let stockOnHandInputs: StockLevelInput[];
+        if (typeof stockOnHandNumberOrInput === 'number') {
+            const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
+            stockOnHandInputs = [
+                { stockLocationId: defaultStockLocation.id, stockOnHand: stockOnHandNumberOrInput },
+            ];
+        } else {
+            stockOnHandInputs = stockOnHandNumberOrInput;
+        }
+        const adjustments: StockAdjustment[] = [];
+        for (const input of stockOnHandInputs) {
+            const stockLevel = await this.stockLevelService.getStockLevel(
+                ctx,
+                productVariantId,
+                input.stockLocationId,
+            );
+            const oldStockLevel = stockLevel.stockOnHand;
+            const newStockLevel = input.stockOnHand;
+            if (oldStockLevel === newStockLevel) {
+                continue;
+            }
+            const delta = newStockLevel - oldStockLevel;
+            const adjustment = await this.connection.getRepository(ctx, StockAdjustment).save(
+                new StockAdjustment({
+                    quantity: delta,
+                    stockLocation: { id: input.stockLocationId },
+                    productVariant: { id: productVariantId },
+                }),
+            );
+            await this.stockLevelService.updateStockOnHandForLocation(
+                ctx,
+                productVariantId,
+                input.stockLocationId,
+                delta,
+            );
+            this.eventBus.publish(new StockMovementEvent(ctx, [adjustment]));
+            adjustments.push(adjustment);
         }
-        const delta = newStockLevel - oldStockLevel;
 
-        const adjustment = await this.connection.getRepository(ctx, StockAdjustment).save(
-            new StockAdjustment({
-                quantity: delta,
-                productVariant: { id: productVariantId },
-            }),
-        );
-        this.eventBus.publish(new StockMovementEvent(ctx, [adjustment]));
-        return adjustment;
+        return adjustments;
     }
 
     /**
@@ -127,18 +165,28 @@ export class StockMovementService {
                 ProductVariant,
                 orderLine.productVariantId,
             );
-            const allocation = new Allocation({
-                productVariant,
-                quantity,
+            const allocationLocations = await this.stockLocationService.getAllocationLocations(
+                ctx,
                 orderLine,
-            });
-            allocations.push(allocation);
+                quantity,
+            );
+            for (const allocationLocation of allocationLocations) {
+                const allocation = new Allocation({
+                    productVariant: new ProductVariant({ id: orderLine.productVariantId }),
+                    stockLocation: allocationLocation.location,
+                    quantity: allocationLocation.quantity,
+                    orderLine,
+                });
+                allocations.push(allocation);
 
-            if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
-                productVariant.stockAllocated += quantity;
-                await this.connection
-                    .getRepository(ctx, ProductVariant)
-                    .save(productVariant, { reload: false });
+                if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
+                    await this.stockLevelService.updateStockAllocatedForLocation(
+                        ctx,
+                        orderLine.productVariantId,
+                        allocationLocation.location.id,
+                        allocationLocation.quantity,
+                    );
+                }
             }
         }
         const savedAllocations = await this.connection.getRepository(ctx, Allocation).save(allocations);
@@ -171,19 +219,34 @@ export class StockMovementService {
                 ProductVariant,
                 orderLine.productVariantId,
             );
-            const sale = new Sale({
-                productVariant,
-                quantity: lineRow.quantity * -1,
+            const saleLocations = await this.stockLocationService.getSaleLocations(
+                ctx,
                 orderLine,
-            });
-            sales.push(sale);
+                lineRow.quantity,
+            );
+            for (const saleLocation of saleLocations) {
+                const sale = new Sale({
+                    productVariant,
+                    quantity: lineRow.quantity * -1,
+                    orderLine,
+                    stockLocation: saleLocation.location,
+                });
+                sales.push(sale);
 
-            if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
-                productVariant.stockOnHand -= lineRow.quantity;
-                productVariant.stockAllocated -= lineRow.quantity;
-                await this.connection
-                    .getRepository(ctx, ProductVariant)
-                    .save(productVariant, { reload: false });
+                if (this.trackInventoryForVariant(productVariant, globalTrackInventory)) {
+                    await this.stockLevelService.updateStockAllocatedForLocation(
+                        ctx,
+                        orderLine.productVariantId,
+                        saleLocation.location.id,
+                        -saleLocation.quantity,
+                    );
+                    await this.stockLevelService.updateStockOnHandForLocation(
+                        ctx,
+                        orderLine.productVariantId,
+                        saleLocation.location.id,
+                        -saleLocation.quantity,
+                    );
+                }
             }
         }
         const savedSales = await this.connection.getRepository(ctx, Sale).save(sales);
@@ -212,23 +275,33 @@ export class StockMovementService {
 
         const cancellations: Cancellation[] = [];
         const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
-        for (const line of orderLines) {
-            const lineInput = lineInputs.find(l => idsAreEqual(l.orderLineId, line.id));
+        for (const orderLine of orderLines) {
+            const lineInput = lineInputs.find(l => idsAreEqual(l.orderLineId, orderLine.id));
             if (!lineInput) {
                 continue;
             }
-            const cancellation = new Cancellation({
-                productVariant: line.productVariant,
-                quantity: lineInput.quantity,
-                orderLine: line,
-            });
-            cancellations.push(cancellation);
+            const cancellationLocations = await this.stockLocationService.getCancellationLocations(
+                ctx,
+                orderLine,
+                lineInput.quantity,
+            );
+            for (const cancellationLocation of cancellationLocations) {
+                const cancellation = new Cancellation({
+                    productVariant: orderLine.productVariant,
+                    quantity: lineInput.quantity,
+                    orderLine,
+                    stockLocation: cancellationLocation.location,
+                });
+                cancellations.push(cancellation);
 
-            if (this.trackInventoryForVariant(line.productVariant, globalTrackInventory)) {
-                line.productVariant.stockOnHand += lineInput.quantity;
-                await this.connection
-                    .getRepository(ctx, ProductVariant)
-                    .save(line.productVariant, { reload: false });
+                if (this.trackInventoryForVariant(orderLine.productVariant, globalTrackInventory)) {
+                    await this.stockLevelService.updateStockOnHandForLocation(
+                        ctx,
+                        orderLine.productVariantId,
+                        cancellationLocation.location.id,
+                        cancellationLocation.quantity,
+                    );
+                }
             }
         }
         const savedCancellations = await this.connection.getRepository(ctx, Cancellation).save(cancellations);
@@ -254,23 +327,32 @@ export class StockMovementService {
         );
         const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
         const variantsMap = new Map<ID, ProductVariant>();
-        for (const line of orderLines) {
-            const lineInput = lineInputs.find(l => idsAreEqual(l.orderLineId, line.id));
+        for (const orderLine of orderLines) {
+            const lineInput = lineInputs.find(l => idsAreEqual(l.orderLineId, orderLine.id));
             if (!lineInput) {
                 continue;
             }
-            const release = new Release({
-                productVariant: line.productVariant,
-                quantity: lineInput.quantity,
-                orderLine: line,
-            });
-            releases.push(release);
-
-            if (this.trackInventoryForVariant(line.productVariant, globalTrackInventory)) {
-                line.productVariant.stockAllocated -= lineInput.quantity;
-                await this.connection
-                    .getRepository(ctx, ProductVariant)
-                    .save(line.productVariant, { reload: false });
+            const releaseLocations = await this.stockLocationService.getReleaseLocations(
+                ctx,
+                orderLine,
+                lineInput.quantity,
+            );
+            for (const releaseLocation of releaseLocations) {
+                const release = new Release({
+                    productVariant: orderLine.productVariant,
+                    quantity: lineInput.quantity,
+                    orderLine,
+                    stockLocation: releaseLocation.location,
+                });
+                releases.push(release);
+                if (this.trackInventoryForVariant(orderLine.productVariant, globalTrackInventory)) {
+                    await this.stockLevelService.updateStockAllocatedForLocation(
+                        ctx,
+                        orderLine.productVariantId,
+                        releaseLocation.location.id,
+                        -releaseLocation.quantity,
+                    );
+                }
             }
         }
         const savedReleases = await this.connection.getRepository(ctx, Release).save(releases);

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

@@ -62,7 +62,7 @@ export const devConfig: VendureConfig = {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
     plugins: [
-        MultivendorPlugin,
+        // MultivendorPlugin,
         AssetServerPlugin.init({
             route: 'assets',
             assetUploadDir: path.join(__dirname, 'assets'),

+ 15 - 27
packages/elasticsearch-plugin/e2e/graphql/generated-e2e-elasticsearch-plugin-types.ts

@@ -768,6 +768,7 @@ export type CreateProductVariantInput = {
     price?: InputMaybe<Scalars['Int']>;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -802,12 +803,8 @@ export type CreateRoleInput = {
     permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-    customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     name: Scalars['String'];
 };
 
@@ -1227,6 +1224,7 @@ export type CustomFields = {
     Promotion: Array<CustomFieldConfig>;
     Seller: Array<CustomFieldConfig>;
     ShippingMethod: Array<CustomFieldConfig>;
+    StockLocation: Array<CustomFieldConfig>;
     TaxCategory: Array<CustomFieldConfig>;
     TaxRate: Array<CustomFieldConfig>;
     User: Array<CustomFieldConfig>;
@@ -2527,7 +2525,6 @@ export type Mutation = {
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
     refundOrder: RefundOrderResult;
-    registerNewSeller?: Maybe<Channel>;
     reindex: Job;
     /** Removes Collections from the specified Channel */
     removeCollectionsFromChannel: Array<Collection>;
@@ -2957,10 +2954,6 @@ export type MutationRefundOrderArgs = {
     input: RefundOrderInput;
 };
 
-export type MutationRegisterNewSellerArgs = {
-    input: RegisterSellerInput;
-};
-
 export type MutationRemoveCollectionsFromChannelArgs = {
     input: RemoveCollectionsFromChannelInput;
 };
@@ -4058,9 +4051,12 @@ export type ProductVariant = Node & {
     product: Product;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    /** @deprecated use stockLevels */
     stockAllocated: Scalars['Int'];
     stockLevel: Scalars['String'];
+    stockLevels: Array<StockLevel>;
     stockMovements: StockMovementList;
+    /** @deprecated use stockLevels */
     stockOnHand: Scalars['Int'];
     taxCategory: TaxCategory;
     taxRateApplied: TaxRate;
@@ -4551,11 +4547,6 @@ export type RefundStateTransitionError = ErrorResult & {
     transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-    administrator: CreateAdministratorInput;
-    shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
     description?: Maybe<Array<LocalizedString>>;
     entity: Scalars['String'];
@@ -4743,18 +4734,13 @@ export type SearchResultSortParameter = {
 
 export type Seller = Node & {
     createdAt: Scalars['DateTime'];
-    customFields?: Maybe<SellerCustomFields>;
+    customFields?: Maybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-    connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-    connectedAccountId?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     name?: InputMaybe<StringOperators>;
@@ -4780,7 +4766,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-    connectedAccountId?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4945,8 +4930,14 @@ export type StockLevel = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+    stockLocationId: Scalars['ID'];
+    stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
     createdAt: Scalars['DateTime'];
+    customFields?: Maybe<Scalars['JSON']>;
     description: Scalars['String'];
     id: Scalars['ID'];
     name: Scalars['String'];
@@ -5402,6 +5393,7 @@ export type UpdateProductVariantInput = {
     outOfStockThreshold?: InputMaybe<Scalars['Int']>;
     price?: InputMaybe<Scalars['Int']>;
     sku?: InputMaybe<Scalars['String']>;
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -5432,12 +5424,8 @@ export type UpdateRoleInput = {
     permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-    customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name?: InputMaybe<Scalars['String']>;
 };

+ 15 - 27
packages/payments-plugin/e2e/graphql/generated-admin-types.ts

@@ -768,6 +768,7 @@ export type CreateProductVariantInput = {
     price?: InputMaybe<Scalars['Int']>;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -802,12 +803,8 @@ export type CreateRoleInput = {
     permissions: Array<Permission>;
 };
 
-export type CreateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type CreateSellerInput = {
-    customFields?: InputMaybe<CreateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     name: Scalars['String'];
 };
 
@@ -1227,6 +1224,7 @@ export type CustomFields = {
     Promotion: Array<CustomFieldConfig>;
     Seller: Array<CustomFieldConfig>;
     ShippingMethod: Array<CustomFieldConfig>;
+    StockLocation: Array<CustomFieldConfig>;
     TaxCategory: Array<CustomFieldConfig>;
     TaxRate: Array<CustomFieldConfig>;
     User: Array<CustomFieldConfig>;
@@ -2527,7 +2525,6 @@ export type Mutation = {
     /** Move a Collection to a different parent or index */
     moveCollection: Collection;
     refundOrder: RefundOrderResult;
-    registerNewSeller?: Maybe<Channel>;
     reindex: Job;
     /** Removes Collections from the specified Channel */
     removeCollectionsFromChannel: Array<Collection>;
@@ -2957,10 +2954,6 @@ export type MutationRefundOrderArgs = {
     input: RefundOrderInput;
 };
 
-export type MutationRegisterNewSellerArgs = {
-    input: RegisterSellerInput;
-};
-
 export type MutationRemoveCollectionsFromChannelArgs = {
     input: RemoveCollectionsFromChannelInput;
 };
@@ -4058,9 +4051,12 @@ export type ProductVariant = Node & {
     product: Product;
     productId: Scalars['ID'];
     sku: Scalars['String'];
+    /** @deprecated use stockLevels */
     stockAllocated: Scalars['Int'];
     stockLevel: Scalars['String'];
+    stockLevels: Array<StockLevel>;
     stockMovements: StockMovementList;
+    /** @deprecated use stockLevels */
     stockOnHand: Scalars['Int'];
     taxCategory: TaxCategory;
     taxRateApplied: TaxRate;
@@ -4551,11 +4547,6 @@ export type RefundStateTransitionError = ErrorResult & {
     transitionError: Scalars['String'];
 };
 
-export type RegisterSellerInput = {
-    administrator: CreateAdministratorInput;
-    shopName: Scalars['String'];
-};
-
 export type RelationCustomFieldConfig = CustomField & {
     description?: Maybe<Array<LocalizedString>>;
     entity: Scalars['String'];
@@ -4743,18 +4734,13 @@ export type SearchResultSortParameter = {
 
 export type Seller = Node & {
     createdAt: Scalars['DateTime'];
-    customFields?: Maybe<SellerCustomFields>;
+    customFields?: Maybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name: Scalars['String'];
     updatedAt: Scalars['DateTime'];
 };
 
-export type SellerCustomFields = {
-    connectedAccountId?: Maybe<Scalars['String']>;
-};
-
 export type SellerFilterParameter = {
-    connectedAccountId?: InputMaybe<StringOperators>;
     createdAt?: InputMaybe<DateOperators>;
     id?: InputMaybe<IdOperators>;
     name?: InputMaybe<StringOperators>;
@@ -4780,7 +4766,6 @@ export type SellerListOptions = {
 };
 
 export type SellerSortParameter = {
-    connectedAccountId?: InputMaybe<SortOrder>;
     createdAt?: InputMaybe<SortOrder>;
     id?: InputMaybe<SortOrder>;
     name?: InputMaybe<SortOrder>;
@@ -4945,8 +4930,14 @@ export type StockLevel = Node & {
     updatedAt: Scalars['DateTime'];
 };
 
+export type StockLevelInput = {
+    stockLocationId: Scalars['ID'];
+    stockOnHand: Scalars['Int'];
+};
+
 export type StockLocation = Node & {
     createdAt: Scalars['DateTime'];
+    customFields?: Maybe<Scalars['JSON']>;
     description: Scalars['String'];
     id: Scalars['ID'];
     name: Scalars['String'];
@@ -5402,6 +5393,7 @@ export type UpdateProductVariantInput = {
     outOfStockThreshold?: InputMaybe<Scalars['Int']>;
     price?: InputMaybe<Scalars['Int']>;
     sku?: InputMaybe<Scalars['String']>;
+    stockLevels?: InputMaybe<Array<StockLevelInput>>;
     stockOnHand?: InputMaybe<Scalars['Int']>;
     taxCategoryId?: InputMaybe<Scalars['ID']>;
     trackInventory?: InputMaybe<GlobalFlag>;
@@ -5432,12 +5424,8 @@ export type UpdateRoleInput = {
     permissions?: InputMaybe<Array<Permission>>;
 };
 
-export type UpdateSellerCustomFieldsInput = {
-    connectedAccountId?: InputMaybe<Scalars['String']>;
-};
-
 export type UpdateSellerInput = {
-    customFields?: InputMaybe<UpdateSellerCustomFieldsInput>;
+    customFields?: InputMaybe<Scalars['JSON']>;
     id: Scalars['ID'];
     name?: InputMaybe<Scalars['String']>;
 };

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff