Kaynağa Gözat

fix(core): Correctly constrain inventory on addItemToOrder mutation

Fixes #691
Michael Bromley 5 yıl önce
ebeveyn
işleme
e4d3aedd0c

+ 46 - 7
packages/core/e2e/stock-control.e2e-spec.ts

@@ -487,7 +487,7 @@ describe('Stock control', () => {
             expect((addItemToOrder as any).order.lines.length).toBe(0);
             expect((addItemToOrder as any).order.lines.length).toBe(0);
         });
         });
 
 
-        it('returns InsufficientStockError when tracking inventory', async () => {
+        it('returns InsufficientStockError when tracking inventory & adding too many at once', async () => {
             const variantId = 'T_1';
             const variantId = 'T_1';
             const { addItemToOrder } = await shopClient.query<
             const { addItemToOrder } = await shopClient.query<
                 AddItemToOrder.Mutation,
                 AddItemToOrder.Mutation,
@@ -789,7 +789,8 @@ describe('Stock control', () => {
         });
         });
 
 
         describe('edge cases', () => {
         describe('edge cases', () => {
-            const variantId = 'T_5';
+            const variant5Id = 'T_5';
+            const variant6Id = 'T_6';
 
 
             beforeAll(async () => {
             beforeAll(async () => {
                 // First place an order which creates a backorder (excess of allocated units)
                 // First place an order which creates a backorder (excess of allocated units)
@@ -798,12 +799,19 @@ describe('Stock control', () => {
                     {
                     {
                         input: [
                         input: [
                             {
                             {
-                                id: variantId,
+                                id: variant5Id,
                                 stockOnHand: 5,
                                 stockOnHand: 5,
                                 outOfStockThreshold: -20,
                                 outOfStockThreshold: -20,
                                 trackInventory: GlobalFlag.TRUE,
                                 trackInventory: GlobalFlag.TRUE,
                                 useGlobalOutOfStockThreshold: false,
                                 useGlobalOutOfStockThreshold: false,
                             },
                             },
+                            {
+                                id: variant6Id,
+                                stockOnHand: 3,
+                                outOfStockThreshold: 0,
+                                trackInventory: GlobalFlag.TRUE,
+                                useGlobalOutOfStockThreshold: false,
+                            },
                         ],
                         ],
                     },
                     },
                 );
                 );
@@ -812,7 +820,7 @@ describe('Stock control', () => {
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Variables
                     AddItemToOrder.Variables
                 >(ADD_ITEM_TO_ORDER, {
                 >(ADD_ITEM_TO_ORDER, {
-                    productVariantId: variantId,
+                    productVariantId: variant5Id,
                     quantity: 25,
                     quantity: 25,
                 });
                 });
                 orderGuard.assertSuccess(add1);
                 orderGuard.assertSuccess(add1);
@@ -827,7 +835,7 @@ describe('Stock control', () => {
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Variables
                     AddItemToOrder.Variables
                 >(ADD_ITEM_TO_ORDER, {
                 >(ADD_ITEM_TO_ORDER, {
-                    productVariantId: variantId,
+                    productVariantId: variant5Id,
                     quantity: 1,
                     quantity: 1,
                 });
                 });
                 orderGuard.assertErrorResult(addItemToOrder);
                 orderGuard.assertErrorResult(addItemToOrder);
@@ -844,7 +852,7 @@ describe('Stock control', () => {
                     {
                     {
                         input: [
                         input: [
                             {
                             {
-                                id: variantId,
+                                id: variant5Id,
                                 outOfStockThreshold: -10,
                                 outOfStockThreshold: -10,
                             },
                             },
                         ],
                         ],
@@ -856,7 +864,7 @@ describe('Stock control', () => {
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Mutation,
                     AddItemToOrder.Variables
                     AddItemToOrder.Variables
                 >(ADD_ITEM_TO_ORDER, {
                 >(ADD_ITEM_TO_ORDER, {
-                    productVariantId: variantId,
+                    productVariantId: variant5Id,
                     quantity: 1,
                     quantity: 1,
                 });
                 });
                 orderGuard.assertErrorResult(addItemToOrder);
                 orderGuard.assertErrorResult(addItemToOrder);
@@ -866,6 +874,37 @@ describe('Stock control', () => {
                     `No items were added to the order due to insufficient stock`,
                     `No items were added to the order due to insufficient stock`,
                 );
                 );
             });
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/691
+            it('returns InsufficientStockError when tracking inventory & adding too many individually', async () => {
+                await shopClient.asAnonymousUser();
+                const { addItemToOrder: add1 } = await shopClient.query<
+                    AddItemToOrder.Mutation,
+                    AddItemToOrder.Variables
+                >(ADD_ITEM_TO_ORDER, {
+                    productVariantId: variant6Id,
+                    quantity: 3,
+                });
+
+                orderGuard.assertSuccess(add1);
+
+                const { addItemToOrder: add2 } = await shopClient.query<
+                    AddItemToOrder.Mutation,
+                    AddItemToOrder.Variables
+                >(ADD_ITEM_TO_ORDER, {
+                    productVariantId: variant6Id,
+                    quantity: 1,
+                });
+
+                orderGuard.assertErrorResult(add2);
+
+                expect(add2.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
+                expect(add2.message).toBe(`No items were added to the order due to insufficient stock`);
+                expect((add2 as any).quantityAvailable).toBe(0);
+                // Still adds as many as available to the Order
+                expect((add2 as any).order.lines[0].productVariant.id).toBe(variant6Id);
+                expect((add2 as any).order.lines[0].quantity).toBe(3);
+            });
         });
         });
     });
     });
 });
 });

+ 25 - 11
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -63,31 +63,45 @@ export class OrderModifier {
      * Ensure that the ProductVariant has sufficient saleable stock to add the given
      * Ensure that the ProductVariant has sufficient saleable stock to add the given
      * quantity to an Order.
      * quantity to an Order.
      */
      */
-    async constrainQuantityToSaleable(ctx: RequestContext, variant: ProductVariant, quantity: number) {
-        let correctedQuantity = quantity;
+    async constrainQuantityToSaleable(
+        ctx: RequestContext,
+        variant: ProductVariant,
+        quantity: number,
+        existingQuantity = 0,
+    ) {
+        let correctedQuantity = quantity + existingQuantity;
         const saleableStockLevel = await this.productVariantService.getSaleableStockLevel(ctx, variant);
         const saleableStockLevel = await this.productVariantService.getSaleableStockLevel(ctx, variant);
         if (saleableStockLevel < correctedQuantity) {
         if (saleableStockLevel < correctedQuantity) {
-            correctedQuantity = Math.max(saleableStockLevel, 0);
+            correctedQuantity = Math.max(saleableStockLevel - existingQuantity, 0);
         }
         }
         return correctedQuantity;
         return correctedQuantity;
     }
     }
 
 
-    /**
-     * Returns the OrderLine to which a new OrderItem belongs, creating a new OrderLine
-     * if no existing line is found.
-     */
-    async getOrCreateItemOrderLine(
+    getExistingOrderLine(
         ctx: RequestContext,
         ctx: RequestContext,
         order: Order,
         order: Order,
         productVariantId: ID,
         productVariantId: ID,
         customFields?: { [key: string]: any },
         customFields?: { [key: string]: any },
-    ) {
-        const existingOrderLine = order.lines.find(line => {
+    ): OrderLine | undefined {
+        return order.lines.find(line => {
             return (
             return (
                 idsAreEqual(line.productVariant.id, productVariantId) &&
                 idsAreEqual(line.productVariant.id, productVariantId) &&
                 this.customFieldsAreEqual(customFields, line.customFields)
                 this.customFieldsAreEqual(customFields, line.customFields)
             );
             );
         });
         });
+    }
+
+    /**
+     * Returns the OrderLine to which a new OrderItem belongs, creating a new OrderLine
+     * if no existing line is found.
+     */
+    async getOrCreateOrderLine(
+        ctx: RequestContext,
+        order: Order,
+        productVariantId: ID,
+        customFields?: { [key: string]: any },
+    ) {
+        const existingOrderLine = this.getExistingOrderLine(ctx, order, productVariantId, customFields);
         if (existingOrderLine) {
         if (existingOrderLine) {
             return existingOrderLine;
             return existingOrderLine;
         }
         }
@@ -210,7 +224,7 @@ export class OrderModifier {
             }
             }
 
 
             const customFields = (row as any).customFields || {};
             const customFields = (row as any).customFields || {};
-            const orderLine = await this.getOrCreateItemOrderLine(ctx, order, productVariantId, customFields);
+            const orderLine = await this.getOrCreateOrderLine(ctx, order, productVariantId, customFields);
             const correctedQuantity = await this.constrainQuantityToSaleable(
             const correctedQuantity = await this.constrainQuantityToSaleable(
                 ctx,
                 ctx,
                 orderLine.productVariant,
                 orderLine.productVariant,

+ 9 - 7
packages/core/src/service/services/order.service.ts

@@ -367,26 +367,28 @@ export class OrderService {
             return validationError;
             return validationError;
         }
         }
         const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId);
         const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId);
+        const existingOrderLine = this.orderModifier.getExistingOrderLine(
+            ctx,
+            order,
+            productVariantId,
+            customFields,
+        );
         const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
         const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
             ctx,
             ctx,
             variant,
             variant,
             quantity,
             quantity,
+            existingOrderLine?.quantity,
         );
         );
         if (correctedQuantity === 0) {
         if (correctedQuantity === 0) {
             return new InsufficientStockError(correctedQuantity, order);
             return new InsufficientStockError(correctedQuantity, order);
         }
         }
-        const orderLine = await this.orderModifier.getOrCreateItemOrderLine(
+        const orderLine = await this.orderModifier.getOrCreateOrderLine(
             ctx,
             ctx,
             order,
             order,
             productVariantId,
             productVariantId,
             customFields,
             customFields,
         );
         );
-        await this.orderModifier.updateOrderLineQuantity(
-            ctx,
-            orderLine,
-            orderLine.quantity + correctedQuantity,
-            order,
-        );
+        await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
         const quantityWasAdjustedDown = correctedQuantity < quantity;
         const quantityWasAdjustedDown = correctedQuantity < quantity;
         const updatedOrder = await this.applyPriceAdjustments(ctx, order, orderLine);
         const updatedOrder = await this.applyPriceAdjustments(ctx, order, orderLine);
         if (quantityWasAdjustedDown) {
         if (quantityWasAdjustedDown) {