Browse Source

feat(core): Create StockMovements when variant stock changed

Relates to #81
Michael Bromley 6 years ago
parent
commit
f8521dbde2

+ 21 - 7
packages/common/src/generated-shop-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-05-02T11:33:31+02:00
+// Generated in 2019-05-02T12:27:28+02:00
 export type Maybe<T> = T | null;
 
 export interface OrderListOptions {
@@ -807,6 +807,20 @@ export interface PaginatedList {
     totalItems: number;
 }
 
+export interface StockMovement {
+    id: string;
+
+    createdAt: DateTime;
+
+    updatedAt: DateTime;
+
+    productVariant: ProductVariant;
+
+    type: StockMovementType;
+
+    quantity: number;
+}
+
 // ====================================================
 // Types
 // ====================================================
@@ -1682,7 +1696,7 @@ export interface AssetList extends PaginatedList {
     totalItems: number;
 }
 
-export interface Cancellation extends Node {
+export interface Cancellation extends Node, StockMovement {
     id: string;
 
     createdAt: DateTime;
@@ -1786,7 +1800,7 @@ export interface PromotionList extends PaginatedList {
     totalItems: number;
 }
 
-export interface Return extends Node {
+export interface Return extends Node, StockMovement {
     id: string;
 
     createdAt: DateTime;
@@ -1808,7 +1822,7 @@ export interface RoleList extends PaginatedList {
     totalItems: number;
 }
 
-export interface Sale extends Node {
+export interface Sale extends Node, StockMovement {
     id: string;
 
     createdAt: DateTime;
@@ -1838,7 +1852,7 @@ export interface ShippingMethodList extends PaginatedList {
     totalItems: number;
 }
 
-export interface StockAdjustment extends Node {
+export interface StockAdjustment extends Node, StockMovement {
     id: string;
 
     createdAt: DateTime;
@@ -1853,7 +1867,7 @@ export interface StockAdjustment extends Node {
 }
 
 export interface StockMovementList {
-    items: StockMovement[];
+    items: StockMovementItem[];
 
     totalItems: number;
 }
@@ -1990,4 +2004,4 @@ export interface ResetPasswordMutationArgs {
 /** The price of a search result product, either as a range or as a single price */
 export type SearchResultPrice = PriceRange | SinglePrice;
 
-export type StockMovement = StockAdjustment | Sale | Cancellation | Return;
+export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;

+ 23 - 7
packages/common/src/generated-types.ts

@@ -1,5 +1,5 @@
 // tslint:disable
-// Generated in 2019-05-02T11:33:32+02:00
+// Generated in 2019-05-02T12:27:29+02:00
 export type Maybe<T> = T | null;
 
 
@@ -4575,6 +4575,22 @@ export interface Node {
 }
 
 
+export interface StockMovement {
+  
+  id: string;
+  
+  createdAt: DateTime;
+  
+  updatedAt: DateTime;
+  
+  productVariant: ProductVariant;
+  
+  type: StockMovementType;
+  
+  quantity: number;
+}
+
+
 
 
 // ====================================================
@@ -5169,13 +5185,13 @@ export interface ProductVariantTranslation {
 
 export interface StockMovementList {
   
-  items: StockMovement[];
+  items: StockMovementItem[];
   
   totalItems: number;
 }
 
 
-export interface StockAdjustment extends Node {
+export interface StockAdjustment extends Node,StockMovement {
   
   id: string;
   
@@ -5191,7 +5207,7 @@ export interface StockAdjustment extends Node {
 }
 
 
-export interface Sale extends Node {
+export interface Sale extends Node,StockMovement {
   
   id: string;
   
@@ -5443,7 +5459,7 @@ export interface ShippingMethod extends Node {
 }
 
 
-export interface Cancellation extends Node {
+export interface Cancellation extends Node,StockMovement {
   
   id: string;
   
@@ -5461,7 +5477,7 @@ export interface Cancellation extends Node {
 }
 
 
-export interface Return extends Node {
+export interface Return extends Node,StockMovement {
   
   id: string;
   
@@ -6418,7 +6434,7 @@ export interface SetUiLanguageMutationArgs {
 
 
 
-export type StockMovement = StockAdjustment | Sale | Cancellation | Return;
+export type StockMovementItem = StockAdjustment | Sale | Cancellation | Return;
 
 /** The price of a search result product, either as a range or as a single price */
 export type SearchResultPrice = PriceRange | SinglePrice;

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

@@ -0,0 +1,146 @@
+import gql from 'graphql-tag';
+import path from 'path';
+
+import { ProductVariant, StockMovementType, UpdateProductVariantInput } from '../../common/src/generated-types';
+
+import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
+import { TestAdminClient, TestShopClient } from './test-client';
+import { TestServer } from './test-server';
+import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
+
+jest.setTimeout(2137 * 1000);
+
+describe('Stock control', () => {
+    const adminClient = new TestAdminClient();
+    const shopClient = new TestShopClient();
+    const server = new TestServer();
+
+    beforeAll(async () => {
+        const token = await server.init(
+            {
+                productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+                customerCount: 2,
+            },
+        );
+        await shopClient.init();
+        await adminClient.init();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    afterAll(async () => {
+        await server.destroy();
+    });
+
+    describe('stock adjustments', () => {
+
+        let variants: ProductVariant[];
+
+        it('stockMovements are initially empty', async () => {
+            const result = await adminClient.query(GET_STOCK_MOVEMENT, { id: 'T_1' });
+
+            variants = result.product.variants;
+            for (const variant of variants) {
+                expect(variant.stockMovements.items).toEqual([]);
+                expect(variant.stockMovements.totalItems).toEqual(0);
+            }
+        });
+
+        it('updating ProductVariant with same stockOnHand does not create a StockMovement', async () => {
+            const result = await adminClient.query(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
+
+            expect(result.updateProductVariants[0].stockMovements.items).toEqual([]);
+            expect(result.updateProductVariants[0].stockMovements.totalItems).toEqual(0);
+        });
+
+        it('increasing stockOnHand creates a StockMovement with correct quantity', async () => {
+            const result = await adminClient.query(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand + 5,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
+
+            expect(result.updateProductVariants[0].stockOnHand).toBe(5);
+            expect(result.updateProductVariants[0].stockMovements.totalItems).toEqual(1);
+            expect(result.updateProductVariants[0].stockMovements.items[0].type).toBe(StockMovementType.ADJUSTMENT);
+            expect(result.updateProductVariants[0].stockMovements.items[0].quantity).toBe(5);
+        });
+
+        it('decreasing stockOnHand creates a StockMovement with correct quantity', async () => {
+            const result = await adminClient.query(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand + 5 - 2,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
+
+            expect(result.updateProductVariants[0].stockOnHand).toBe(3);
+            expect(result.updateProductVariants[0].stockMovements.totalItems).toEqual(2);
+            expect(result.updateProductVariants[0].stockMovements.items[1].type).toBe(StockMovementType.ADJUSTMENT);
+            expect(result.updateProductVariants[0].stockMovements.items[1].quantity).toBe(-2);
+        });
+
+        it('attempting to set a negative stockOnHand throws', assertThrowsWithMessage(
+            async () => {
+                const result = await adminClient.query(UPDATE_STOCK_ON_HAND, {
+                    input: [
+                        {
+                            id: variants[0].id,
+                            stockOnHand: -1,
+                        },
+                    ] as UpdateProductVariantInput[],
+                });
+            },
+            'stockOnHand cannot be a negative value'),
+        );
+    });
+
+});
+
+const VARIANT_WITH_STOCK_FRAGMENT = gql`
+    fragment VariantWithStock on ProductVariant {
+        id
+        stockOnHand
+        stockMovements {
+            items {
+                ...on StockMovement {
+                    id
+                    type
+                    quantity
+                }
+            }
+            totalItems
+        }
+    }
+`;
+
+const GET_STOCK_MOVEMENT = gql`
+    query ($id: ID!) {
+        product(id: $id) {
+            id
+            variants {
+                ...VariantWithStock
+            }
+        }
+    }
+    ${VARIANT_WITH_STOCK_FRAGMENT}
+`;
+
+const UPDATE_STOCK_ON_HAND = gql`
+    mutation ($input: [UpdateProductVariantInput!]!) {
+        updateProductVariants(input: $input) {
+            ...VariantWithStock
+        }
+    }
+    ${VARIANT_WITH_STOCK_FRAGMENT}
+`;

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

@@ -34,7 +34,7 @@ import { OrderEntityResolver } from './resolvers/entity/order-entity.resolver';
 import { OrderLineEntityResolver } from './resolvers/entity/order-line-entity.resolver';
 import { ProductEntityResolver } from './resolvers/entity/product-entity.resolver';
 import { ProductOptionGroupEntityResolver } from './resolvers/entity/product-option-group-entity.resolver';
-import { ProductVariantEntityResolver } from './resolvers/entity/product-variant-entity.resolver';
+import { ProductVariantAdminEntityResolver, ProductVariantEntityResolver } from './resolvers/entity/product-variant-entity.resolver';
 import { ShopAuthResolver } from './resolvers/shop/shop-auth.resolver';
 import { ShopCustomerResolver } from './resolvers/shop/shop-customer.resolver';
 import { ShopEnvironmentResolver } from './resolvers/shop/shop-environment.resolver';
@@ -84,6 +84,10 @@ export const entityResolvers = [
     ProductVariantEntityResolver,
 ];
 
+export const adminEntityResolvers = [
+    ProductVariantAdminEntityResolver,
+];
+
 /**
  * The internal module containing some shared providers used by more than
  * one API module.
@@ -100,7 +104,7 @@ export class ApiSharedModule {}
  */
 @Module({
     imports: [ApiSharedModule, PluginModule, ServiceModule, DataImportModule],
-    providers: [...adminResolvers, ...entityResolvers, ...PluginModule.adminApiResolvers()],
+    providers: [...adminResolvers, ...entityResolvers, ...adminEntityResolvers, ...PluginModule.adminApiResolvers()],
     exports: adminResolvers,
 })
 export class AdminApiModule {}

+ 17 - 14
packages/core/src/api/config/configure-graphql-module.ts

@@ -68,6 +68,21 @@ async function createGraphQLOptions(
         },
     };
 
+    const stockMovementResolveType = {
+        __resolveType(value: any) {
+            switch (value.type) {
+                case StockMovementType.ADJUSTMENT:
+                    return 'StockAdjustment';
+                case StockMovementType.SALE:
+                    return 'Sale';
+                case StockMovementType.CANCELLATION:
+                    return 'Cancellation';
+                case StockMovementType.RETURN:
+                    return 'Return';
+            }
+        },
+    };
+
     return {
         path: '/' + options.apiPath,
         typeDefs: await createTypeDefs(options.apiType),
@@ -83,20 +98,8 @@ async function createGraphQLOptions(
                     return value.hasOwnProperty('value') ? 'SinglePrice' : 'PriceRange';
                 },
             },
-            StockMovement: {
-                __resolveType(value: any) {
-                    switch (value.type) {
-                        case StockMovementType.ADJUSTMENT:
-                            return 'StockAdjustment';
-                        case StockMovementType.SALE:
-                            return 'Sale';
-                        case StockMovementType.CANCELLATION:
-                            return 'Cancellation';
-                        case StockMovementType.RETURN:
-                            return 'Return';
-                    }
-                },
-            },
+            StockMovementItem: stockMovementResolveType,
+            StockMovement: stockMovementResolveType,
         },
         uploads: {
             maxFileSize: configService.assetOptions.uploadMaxFileSize,

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

@@ -1,9 +1,13 @@
-import { Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { Args, Parent, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { StockMovementListOptions } from '@vendure/common/lib/generated-types';
+import { PaginatedList } from '@vendure/common/src/shared-types';
 
 import { Translated } from '../../../common/types/locale-types';
 import { FacetValue, ProductOption } from '../../../entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
+import { StockMovement } from '../../../entity/stock-movement/stock-movement.entity';
 import { ProductVariantService } from '../../../service/services/product-variant.service';
+import { StockMovementService } from '../../../service/services/stock-movement.service';
 import { ApiType } from '../../common/get-api-type';
 import { RequestContext } from '../../common/request-context';
 import { Api } from '../../decorators/api.decorator';
@@ -42,3 +46,17 @@ export class ProductVariantEntityResolver {
         return facetValues;
     }
 }
+
+@Resolver('ProductVariant')
+export class ProductVariantAdminEntityResolver {
+    constructor(private stockMovementService: StockMovementService) {}
+
+    @ResolveProperty()
+    async stockMovements(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+        @Args() args: { options: StockMovementListOptions },
+    ): Promise<PaginatedList<StockMovement>> {
+        return this.stockMovementService.getStockMovementsByProductVariantId(ctx, productVariant.id, args.options);
+    }
+}

+ 14 - 14
packages/core/src/api/schema/type/stock-movement.type.graphql

@@ -5,16 +5,16 @@ enum StockMovementType {
     RETURN
 }
 
-# interface StockMovement {
-#     id: ID!
-#     createdAt: DateTime!
-#     updatedAt: DateTime!
-#     productVariant: ProductVariant!
-#     type: StockMovementType!
-#     quantity: Int!
-# }
+interface StockMovement {
+    id: ID!
+    createdAt: DateTime!
+    updatedAt: DateTime!
+    productVariant: ProductVariant!
+    type: StockMovementType!
+    quantity: Int!
+}
 
-type StockAdjustment implements Node {
+type StockAdjustment implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
@@ -23,7 +23,7 @@ type StockAdjustment implements Node {
     quantity: Int!
 }
 
-type Sale implements Node {
+type Sale implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
@@ -33,7 +33,7 @@ type Sale implements Node {
     orderLine: OrderLine!
 }
 
-type Cancellation implements Node {
+type Cancellation implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
@@ -43,7 +43,7 @@ type Cancellation implements Node {
     orderLine: OrderLine!
 }
 
-type Return implements Node {
+type Return implements Node & StockMovement {
     id: ID!
     createdAt: DateTime!
     updatedAt: DateTime!
@@ -53,9 +53,9 @@ type Return implements Node {
     orderItem: OrderItem!
 }
 
-union StockMovement = StockAdjustment | Sale | Cancellation | Return
+union StockMovementItem = StockAdjustment | Sale | Cancellation | Return
 
 type StockMovementList {
-    items: [StockMovement!]!
+    items: [StockMovementItem!]!
     totalItems: Int!
 }

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

@@ -1,3 +1,4 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
 import { ChildEntity, ManyToOne } from 'typeorm';
 
@@ -7,6 +8,8 @@ import { StockMovement } from './stock-movement.entity';
 
 @ChildEntity()
 export class Cancellation extends StockMovement {
+    readonly type = StockMovementType.CANCELLATION;
+
     constructor(input: DeepPartial<Cancellation>) {
         super(input);
     }

+ 9 - 1
packages/core/src/entity/stock-movement/return.entity.ts

@@ -1,11 +1,19 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity } from 'typeorm';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { OrderItem } from '../order-item/order-item.entity';
 
 import { StockMovement } from './stock-movement.entity';
 
 @ChildEntity()
 export class Return extends StockMovement {
+    readonly type = StockMovementType.RETURN;
+
     constructor(input: DeepPartial<Return>) {
         super(input);
     }
+
+    @ManyToOne(type => OrderItem)
+    orderItem: OrderItem;
 }

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

@@ -1,3 +1,4 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
 import { ChildEntity, ManyToOne } from 'typeorm';
 
@@ -7,6 +8,8 @@ import { StockMovement } from './stock-movement.entity';
 
 @ChildEntity()
 export class Sale extends StockMovement {
+    readonly type = StockMovementType.SALE;
+
     constructor(input: DeepPartial<Sale>) {
         super(input);
     }

+ 4 - 6
packages/core/src/entity/stock-movement/stock-adjustment.entity.ts

@@ -1,16 +1,14 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { ChildEntity, ManyToOne } from 'typeorm';
-
-import { OrderItem } from '../order-item/order-item.entity';
+import { ChildEntity } from 'typeorm';
 
 import { StockMovement } from './stock-movement.entity';
 
 @ChildEntity()
 export class StockAdjustment extends StockMovement {
+    readonly type = StockMovementType.ADJUSTMENT;
+
     constructor(input: DeepPartial<StockAdjustment>) {
         super(input);
     }
-
-    @ManyToOne(type => OrderItem)
-    orderItem: OrderItem;
 }

+ 4 - 3
packages/core/src/entity/stock-movement/stock-movement.entity.ts

@@ -1,3 +1,4 @@
+import { StockMovementType } from '@vendure/common/lib/generated-types';
 import { Column, Entity, ManyToOne, TableInheritance } from 'typeorm';
 
 import { VendureEntity } from '../base/base.entity';
@@ -11,10 +12,10 @@ import { ProductVariant } from '../product-variant/product-variant.entity';
  * @docsCategory entities
  */
 @Entity()
-@TableInheritance({ column: { type: 'varchar', name: 'type' } })
+@TableInheritance({ column: { type: 'varchar', name: 'discriminator' } })
 export abstract class StockMovement extends VendureEntity {
-    @Column({ nullable: false })
-    type: string;
+    @Column({ nullable: false, type: 'varchar' })
+    readonly type: StockMovementType;
 
     @ManyToOne(type => ProductVariant, variant => variant.stockMovements)
     productVariant: ProductVariant;

+ 1 - 0
packages/core/src/i18n/messages/en.json

@@ -25,6 +25,7 @@
     "payment-may-only-be-added-in-arrangingpayment-state": "A Payment may only be added when Order is in \"ArrangingPayment\" state",
     "password-reset-token-has-expired": "Password reset token has expired.",
     "password-reset-token-not-recognized": "Password reset token not recognized",
+    "stockonhand-cannot-be-negative": "stockOnHand cannot be a negative value",
     "verification-token-has-expired": "Verification token has expired. Use refreshCustomerVerification to send a new token.",
     "verification-token-not-recognized": "Verification token not recognized",
     "unauthorized": "The credentials did not match. Please check and try again",

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

@@ -36,6 +36,7 @@ import { PromotionService } from './services/promotion.service';
 import { RoleService } from './services/role.service';
 import { SearchService } from './services/search.service';
 import { ShippingMethodService } from './services/shipping-method.service';
+import { StockMovementService } from './services/stock-movement.service';
 import { TaxCategoryService } from './services/tax-category.service';
 import { TaxRateService } from './services/tax-rate.service';
 import { UserService } from './services/user.service';
@@ -63,6 +64,7 @@ const exportedProviders = [
     RoleService,
     ShippingMethodService,
     SearchService,
+    StockMovementService,
     TaxCategoryService,
     TaxRateService,
     UserService,

+ 10 - 2
packages/core/src/service/services/product-variant.service.ts

@@ -7,7 +7,7 @@ import { Connection } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
-import { EntityNotFoundError, InternalServerError } from '../../common/error/errors';
+import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
@@ -29,6 +29,7 @@ import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { FacetValueService } from './facet-value.service';
 import { GlobalSettingsService } from './global-settings.service';
+import { StockMovementService } from './stock-movement.service';
 import { TaxCategoryService } from './tax-category.service';
 import { TaxRateService } from './tax-rate.service';
 import { ZoneService } from './zone.service';
@@ -48,6 +49,7 @@ export class ProductVariantService {
         private eventBus: EventBus,
         private listQueryBuilder: ListQueryBuilder,
         private globalSettingsService: GlobalSettingsService,
+        private stockMovementService: StockMovementService,
     ) {}
 
     findOne(ctx: RequestContext, productVariantId: ID): Promise<Translated<ProductVariant> | undefined> {
@@ -172,7 +174,10 @@ export class ProductVariantService {
     }
 
     async update(ctx: RequestContext, input: UpdateProductVariantInput): Promise<Translated<ProductVariant>> {
-        await getEntityOrThrow(this.connection, ProductVariant, input.id);
+        const existingVariant = await getEntityOrThrow(this.connection, ProductVariant, input.id);
+        if (input.stockOnHand && input.stockOnHand < 0) {
+            throw new UserInputError('error.stockonhand-cannot-be-negative');
+        }
         await this.translatableSaver.update({
             input,
             entityType: ProductVariant,
@@ -187,6 +192,9 @@ export class ProductVariantService {
                 if (input.facetValueIds) {
                     updatedVariant.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
                 }
+                if (input.stockOnHand != null) {
+                    await this.stockMovementService.adjustProductVariantStock(existingVariant, input.stockOnHand);
+                }
                 await this.assetUpdater.updateEntityAssets(updatedVariant, input);
             },
             typeOrmSubscriberData: {

+ 57 - 0
packages/core/src/service/services/stock-movement.service.ts

@@ -0,0 +1,57 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { StockMovementListOptions } from '@vendure/common/lib/generated-types';
+import { Connection } from 'typeorm';
+
+import { ID, PaginatedList } from '../../../../common/lib/shared-types';
+import { RequestContext } from '../../api/common/request-context';
+import { ShippingCalculator } from '../../config/shipping-method/shipping-calculator';
+import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
+import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
+import { StockAdjustment } from '../../entity/stock-movement/stock-adjustment.entity';
+import { StockMovement } from '../../entity/stock-movement/stock-movement.entity';
+import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+
+@Injectable()
+export class StockMovementService {
+    shippingEligibilityCheckers: ShippingEligibilityChecker[];
+    shippingCalculators: ShippingCalculator[];
+    private activeShippingMethods: ShippingMethod[];
+
+    constructor(
+        @InjectConnection() private connection: Connection,
+        private listQueryBuilder: ListQueryBuilder,
+    ) {}
+
+    getStockMovementsByProductVariantId(
+        ctx: RequestContext,
+        productVariantId: ID,
+        options: StockMovementListOptions,
+    ): Promise<PaginatedList<StockMovement>> {
+        return this.listQueryBuilder
+            .build<StockMovement>(StockMovement as any, options)
+            .leftJoin('stockmovement.productVariant', 'productVariant')
+            .andWhere('productVariant.id = :productVariantId', { productVariantId })
+            .getManyAndCount()
+            .then(async ([items, totalItems]) => {
+                return {
+                    items,
+                    totalItems,
+                };
+            });
+    }
+
+    async adjustProductVariantStock(variant: ProductVariant, newStockLevel: number): Promise<StockAdjustment | undefined> {
+        if (variant.stockOnHand === newStockLevel) {
+            return;
+        }
+        const delta = newStockLevel - variant.stockOnHand;
+
+        const adjustment = new StockAdjustment({
+            quantity: delta,
+            productVariant: variant,
+        });
+        return this.connection.getRepository(StockAdjustment).save(adjustment);
+    }
+}

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


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


+ 150 - 2
schema.json

@@ -21827,7 +21827,7 @@
                   "name": null,
                   "ofType": {
                     "kind": "UNION",
-                    "name": "StockMovement",
+                    "name": "StockMovementItem",
                     "ofType": null
                   }
                 }
@@ -21860,7 +21860,7 @@
       },
       {
         "kind": "UNION",
-        "name": "StockMovement",
+        "name": "StockMovementItem",
         "description": null,
         "fields": null,
         "inputFields": null,
@@ -21997,11 +21997,144 @@
             "kind": "INTERFACE",
             "name": "Node",
             "ofType": null
+          },
+          {
+            "kind": "INTERFACE",
+            "name": "StockMovement",
+            "ofType": null
           }
         ],
         "enumValues": null,
         "possibleTypes": null
       },
+      {
+        "kind": "INTERFACE",
+        "name": "StockMovement",
+        "description": null,
+        "fields": [
+          {
+            "name": "id",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "ID",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "createdAt",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "DateTime",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "updatedAt",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "DateTime",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "productVariant",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "OBJECT",
+                "name": "ProductVariant",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "type",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "ENUM",
+                "name": "StockMovementType",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          },
+          {
+            "name": "quantity",
+            "description": null,
+            "args": [],
+            "type": {
+              "kind": "NON_NULL",
+              "name": null,
+              "ofType": {
+                "kind": "SCALAR",
+                "name": "Int",
+                "ofType": null
+              }
+            },
+            "isDeprecated": false,
+            "deprecationReason": null
+          }
+        ],
+        "inputFields": null,
+        "interfaces": null,
+        "enumValues": null,
+        "possibleTypes": [
+          {
+            "kind": "OBJECT",
+            "name": "StockAdjustment",
+            "ofType": null
+          },
+          {
+            "kind": "OBJECT",
+            "name": "Sale",
+            "ofType": null
+          },
+          {
+            "kind": "OBJECT",
+            "name": "Cancellation",
+            "ofType": null
+          },
+          {
+            "kind": "OBJECT",
+            "name": "Return",
+            "ofType": null
+          }
+        ]
+      },
       {
         "kind": "OBJECT",
         "name": "Sale",
@@ -22126,6 +22259,11 @@
             "kind": "INTERFACE",
             "name": "Node",
             "ofType": null
+          },
+          {
+            "kind": "INTERFACE",
+            "name": "StockMovement",
+            "ofType": null
           }
         ],
         "enumValues": null,
@@ -22255,6 +22393,11 @@
             "kind": "INTERFACE",
             "name": "Node",
             "ofType": null
+          },
+          {
+            "kind": "INTERFACE",
+            "name": "StockMovement",
+            "ofType": null
           }
         ],
         "enumValues": null,
@@ -22384,6 +22527,11 @@
             "kind": "INTERFACE",
             "name": "Node",
             "ofType": null
+          },
+          {
+            "kind": "INTERFACE",
+            "name": "StockMovement",
+            "ofType": null
           }
         ],
         "enumValues": null,

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