Skip to content

Commit

Permalink
fix(core): Correctly constrain inventory on addItemToOrder mutation
Browse files Browse the repository at this point in the history
Fixes #691
  • Loading branch information
michaelbromley committed Feb 9, 2021
1 parent e5182b7 commit e4d3aed
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 25 deletions.
53 changes: 46 additions & 7 deletions packages/core/e2e/stock-control.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ describe('Stock control', () => {
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 { addItemToOrder } = await shopClient.query<
AddItemToOrder.Mutation,
Expand Down Expand Up @@ -789,7 +789,8 @@ describe('Stock control', () => {
});

describe('edge cases', () => {
const variantId = 'T_5';
const variant5Id = 'T_5';
const variant6Id = 'T_6';

beforeAll(async () => {
// First place an order which creates a backorder (excess of allocated units)
Expand All @@ -798,12 +799,19 @@ describe('Stock control', () => {
{
input: [
{
id: variantId,
id: variant5Id,
stockOnHand: 5,
outOfStockThreshold: -20,
trackInventory: GlobalFlag.TRUE,
useGlobalOutOfStockThreshold: false,
},
{
id: variant6Id,
stockOnHand: 3,
outOfStockThreshold: 0,
trackInventory: GlobalFlag.TRUE,
useGlobalOutOfStockThreshold: false,
},
],
},
);
Expand All @@ -812,7 +820,7 @@ describe('Stock control', () => {
AddItemToOrder.Mutation,
AddItemToOrder.Variables
>(ADD_ITEM_TO_ORDER, {
productVariantId: variantId,
productVariantId: variant5Id,
quantity: 25,
});
orderGuard.assertSuccess(add1);
Expand All @@ -827,7 +835,7 @@ describe('Stock control', () => {
AddItemToOrder.Mutation,
AddItemToOrder.Variables
>(ADD_ITEM_TO_ORDER, {
productVariantId: variantId,
productVariantId: variant5Id,
quantity: 1,
});
orderGuard.assertErrorResult(addItemToOrder);
Expand All @@ -844,7 +852,7 @@ describe('Stock control', () => {
{
input: [
{
id: variantId,
id: variant5Id,
outOfStockThreshold: -10,
},
],
Expand All @@ -856,7 +864,7 @@ describe('Stock control', () => {
AddItemToOrder.Mutation,
AddItemToOrder.Variables
>(ADD_ITEM_TO_ORDER, {
productVariantId: variantId,
productVariantId: variant5Id,
quantity: 1,
});
orderGuard.assertErrorResult(addItemToOrder);
Expand All @@ -866,6 +874,37 @@ describe('Stock control', () => {
`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);
});
});
});
});
Expand Down
36 changes: 25 additions & 11 deletions packages/core/src/service/helpers/order-modifier/order-modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,31 +63,45 @@ export class OrderModifier {
* Ensure that the ProductVariant has sufficient saleable stock to add the given
* 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);
if (saleableStockLevel < correctedQuantity) {
correctedQuantity = Math.max(saleableStockLevel, 0);
correctedQuantity = Math.max(saleableStockLevel - existingQuantity, 0);
}
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,
order: Order,
productVariantId: ID,
customFields?: { [key: string]: any },
) {
const existingOrderLine = order.lines.find(line => {
): OrderLine | undefined {
return order.lines.find(line => {
return (
idsAreEqual(line.productVariant.id, productVariantId) &&
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) {
return existingOrderLine;
}
Expand Down Expand Up @@ -210,7 +224,7 @@ export class OrderModifier {
}

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(
ctx,
orderLine.productVariant,
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/service/services/order.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,26 +367,28 @@ export class OrderService {
return validationError;
}
const variant = await this.connection.getEntityOrThrow(ctx, ProductVariant, productVariantId);
const existingOrderLine = this.orderModifier.getExistingOrderLine(
ctx,
order,
productVariantId,
customFields,
);
const correctedQuantity = await this.orderModifier.constrainQuantityToSaleable(
ctx,
variant,
quantity,
existingOrderLine?.quantity,
);
if (correctedQuantity === 0) {
return new InsufficientStockError(correctedQuantity, order);
}
const orderLine = await this.orderModifier.getOrCreateItemOrderLine(
const orderLine = await this.orderModifier.getOrCreateOrderLine(
ctx,
order,
productVariantId,
customFields,
);
await this.orderModifier.updateOrderLineQuantity(
ctx,
orderLine,
orderLine.quantity + correctedQuantity,
order,
);
await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
const quantityWasAdjustedDown = correctedQuantity < quantity;
const updatedOrder = await this.applyPriceAdjustments(ctx, order, orderLine);
if (quantityWasAdjustedDown) {
Expand Down

0 comments on commit e4d3aed

Please sign in to comment.