|
|
@@ -13,27 +13,36 @@ import {
|
|
|
CancelOrder,
|
|
|
CreateAddressInput,
|
|
|
CreateFulfillment,
|
|
|
+ ErrorCode as AdminErrorCode,
|
|
|
+ FulfillmentFragment,
|
|
|
GetOrder,
|
|
|
GetStockMovement,
|
|
|
GlobalFlag,
|
|
|
StockMovementType,
|
|
|
+ UpdateGlobalSettings,
|
|
|
UpdateProductVariantInput,
|
|
|
+ UpdateProductVariants,
|
|
|
UpdateStock,
|
|
|
VariantWithStockFragment,
|
|
|
} from './graphql/generated-e2e-admin-types';
|
|
|
import {
|
|
|
AddItemToOrder,
|
|
|
AddPaymentToOrder,
|
|
|
+ ErrorCode,
|
|
|
PaymentInput,
|
|
|
SetShippingAddress,
|
|
|
TestOrderFragmentFragment,
|
|
|
+ TestOrderWithPaymentsFragment,
|
|
|
TransitionToState,
|
|
|
+ UpdatedOrderFragment,
|
|
|
} from './graphql/generated-e2e-shop-types';
|
|
|
import {
|
|
|
CANCEL_ORDER,
|
|
|
CREATE_FULFILLMENT,
|
|
|
GET_ORDER,
|
|
|
GET_STOCK_MOVEMENT,
|
|
|
+ UPDATE_GLOBAL_SETTINGS,
|
|
|
+ UPDATE_PRODUCT_VARIANTS,
|
|
|
} from './graphql/shared-definitions';
|
|
|
import {
|
|
|
ADD_ITEM_TO_ORDER,
|
|
|
@@ -42,6 +51,7 @@ import {
|
|
|
TRANSITION_TO_STATE,
|
|
|
} from './graphql/shop-definitions';
|
|
|
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
|
|
|
+import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
|
|
|
|
|
|
describe('Stock control', () => {
|
|
|
const { server, adminClient, shopClient } = createTestEnvironment(
|
|
|
@@ -52,15 +62,27 @@ describe('Stock control', () => {
|
|
|
}),
|
|
|
);
|
|
|
|
|
|
- const orderGuard: ErrorResultGuard<TestOrderFragmentFragment> = createErrorResultGuard<
|
|
|
- TestOrderFragmentFragment
|
|
|
- >(input => !!input.lines);
|
|
|
+ const orderGuard: ErrorResultGuard<
|
|
|
+ TestOrderFragmentFragment | UpdatedOrderFragment
|
|
|
+ > = createErrorResultGuard<TestOrderFragmentFragment>(input => !!input.lines);
|
|
|
+
|
|
|
+ const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard<
|
|
|
+ FulfillmentFragment
|
|
|
+ >(input => !!input.state);
|
|
|
+
|
|
|
+ async function getProductWithStockMovement(productId: string) {
|
|
|
+ const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
+ GET_STOCK_MOVEMENT,
|
|
|
+ { id: productId },
|
|
|
+ );
|
|
|
+ return product;
|
|
|
+ }
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
await server.init({
|
|
|
initialData,
|
|
|
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
|
|
|
- customerCount: 2,
|
|
|
+ customerCount: 3,
|
|
|
});
|
|
|
await adminClient.asSuperAdmin();
|
|
|
}, TEST_SETUP_TIMEOUT_MS);
|
|
|
@@ -162,10 +184,7 @@ describe('Stock control', () => {
|
|
|
let orderId: string;
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(UPDATE_STOCK_ON_HAND, {
|
|
|
@@ -218,7 +237,7 @@ describe('Stock control', () => {
|
|
|
});
|
|
|
|
|
|
it('creates an Allocation when order completed', async () => {
|
|
|
- const { addPaymentToOrder } = await shopClient.query<
|
|
|
+ const { addPaymentToOrder: order } = await shopClient.query<
|
|
|
AddPaymentToOrder.Mutation,
|
|
|
AddPaymentToOrder.Variables
|
|
|
>(ADD_PAYMENT, {
|
|
|
@@ -227,14 +246,11 @@ describe('Stock control', () => {
|
|
|
metadata: {},
|
|
|
} as PaymentInput,
|
|
|
});
|
|
|
- orderGuard.assertSuccess(addPaymentToOrder);
|
|
|
- expect(addPaymentToOrder).not.toBeNull();
|
|
|
- orderId = addPaymentToOrder.id;
|
|
|
+ orderGuard.assertSuccess(order);
|
|
|
+ expect(order).not.toBeNull();
|
|
|
+ orderId = order.id;
|
|
|
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
expect(variant1.stockMovements.totalItems).toBe(2);
|
|
|
@@ -251,10 +267,7 @@ describe('Stock control', () => {
|
|
|
});
|
|
|
|
|
|
it('stockAllocated is updated according to trackInventory setting', async () => {
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
// stockOnHand not changed yet
|
|
|
@@ -280,10 +293,7 @@ describe('Stock control', () => {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [_, variant2, __] = product!.variants;
|
|
|
|
|
|
expect(variant2.stockMovements.totalItems).toBe(3);
|
|
|
@@ -309,10 +319,7 @@ describe('Stock control', () => {
|
|
|
},
|
|
|
);
|
|
|
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
expect(variant1.stockMovements.totalItems).toBe(3);
|
|
|
@@ -330,10 +337,7 @@ describe('Stock control', () => {
|
|
|
});
|
|
|
|
|
|
it('updates stockOnHand and stockAllocated when Sales are created', async () => {
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
expect(variant1.stockOnHand).toBe(5); // untracked inventory
|
|
|
@@ -358,10 +362,7 @@ describe('Stock control', () => {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
- const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
|
|
|
- GET_STOCK_MOVEMENT,
|
|
|
- { id: 'T_2' },
|
|
|
- );
|
|
|
+ const product = await getProductWithStockMovement('T_2');
|
|
|
const [variant1, variant2, variant3] = product!.variants;
|
|
|
|
|
|
expect(variant1.stockMovements.totalItems).toBe(5);
|
|
|
@@ -379,6 +380,395 @@ describe('Stock control', () => {
|
|
|
expect(variant3.stockMovements.items[6].type).toBe(StockMovementType.CANCELLATION);
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+ describe('saleable stock level', () => {
|
|
|
+ let order: TestOrderWithPaymentsFragment;
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ await adminClient.query<UpdateGlobalSettings.Mutation, UpdateGlobalSettings.Variables>(
|
|
|
+ UPDATE_GLOBAL_SETTINGS,
|
|
|
+ {
|
|
|
+ input: {
|
|
|
+ trackInventory: true,
|
|
|
+ outOfStockThreshold: -5,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
|
|
|
+ UPDATE_PRODUCT_VARIANTS,
|
|
|
+ {
|
|
|
+ input: [
|
|
|
+ {
|
|
|
+ id: 'T_1',
|
|
|
+ stockOnHand: 3,
|
|
|
+ outOfStockThreshold: 0,
|
|
|
+ trackInventory: GlobalFlag.TRUE,
|
|
|
+ useGlobalOutOfStockThreshold: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'T_2',
|
|
|
+ stockOnHand: 3,
|
|
|
+ outOfStockThreshold: 0,
|
|
|
+ trackInventory: GlobalFlag.FALSE,
|
|
|
+ useGlobalOutOfStockThreshold: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'T_3',
|
|
|
+ stockOnHand: 3,
|
|
|
+ outOfStockThreshold: 2,
|
|
|
+ trackInventory: GlobalFlag.TRUE,
|
|
|
+ useGlobalOutOfStockThreshold: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'T_4',
|
|
|
+ stockOnHand: 3,
|
|
|
+ outOfStockThreshold: 0,
|
|
|
+ trackInventory: GlobalFlag.TRUE,
|
|
|
+ useGlobalOutOfStockThreshold: true,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
|
|
|
+ });
|
|
|
+
|
|
|
+ it('returns InsufficientStockError when tracking inventory', async () => {
|
|
|
+ const variantId = 'T_1';
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 5,
|
|
|
+ });
|
|
|
+
|
|
|
+ orderGuard.assertErrorResult(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
|
|
|
+ expect(addItemToOrder.message).toBe(
|
|
|
+ `Only 3 items were added to the order due to insufficient stock`,
|
|
|
+ );
|
|
|
+ expect((addItemToOrder as any).quantityAvailable).toBe(3);
|
|
|
+ // Still adds as many as available to the Order
|
|
|
+ expect((addItemToOrder as any).order.lines[0].productVariant.id).toBe(variantId);
|
|
|
+ expect((addItemToOrder as any).order.lines[0].quantity).toBe(3);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[0];
|
|
|
+
|
|
|
+ expect(variant.id).toBe(variantId);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ expect(variant.stockOnHand).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('does not return error when not tracking inventory', async () => {
|
|
|
+ const variantId = 'T_2';
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 5,
|
|
|
+ });
|
|
|
+
|
|
|
+ orderGuard.assertSuccess(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.lines.length).toBe(2);
|
|
|
+ expect(addItemToOrder.lines[1].productVariant.id).toBe(variantId);
|
|
|
+ expect(addItemToOrder.lines[1].quantity).toBe(5);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[1];
|
|
|
+
|
|
|
+ expect(variant.id).toBe(variantId);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ expect(variant.stockOnHand).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('returns InsufficientStockError for positive threshold', async () => {
|
|
|
+ const variantId = 'T_3';
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 2,
|
|
|
+ });
|
|
|
+
|
|
|
+ orderGuard.assertErrorResult(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
|
|
|
+ expect(addItemToOrder.message).toBe(
|
|
|
+ `Only 1 item was added to the order due to insufficient stock`,
|
|
|
+ );
|
|
|
+ expect((addItemToOrder as any).quantityAvailable).toBe(1);
|
|
|
+ // Still adds as many as available to the Order
|
|
|
+ expect((addItemToOrder as any).order.lines.length).toBe(3);
|
|
|
+ expect((addItemToOrder as any).order.lines[2].productVariant.id).toBe(variantId);
|
|
|
+ expect((addItemToOrder as any).order.lines[2].quantity).toBe(1);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[2];
|
|
|
+
|
|
|
+ expect(variant.id).toBe(variantId);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ expect(variant.stockOnHand).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('negative threshold allows backorder', async () => {
|
|
|
+ const variantId = 'T_4';
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 8,
|
|
|
+ });
|
|
|
+
|
|
|
+ orderGuard.assertSuccess(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.lines.length).toBe(4);
|
|
|
+ expect(addItemToOrder.lines[3].productVariant.id).toBe(variantId);
|
|
|
+ expect(addItemToOrder.lines[3].quantity).toBe(8);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[3];
|
|
|
+
|
|
|
+ expect(variant.id).toBe(variantId);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ expect(variant.stockOnHand).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('allocates stock', async () => {
|
|
|
+ await proceedToArrangingPayment(shopClient);
|
|
|
+ const result = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
|
|
|
+ orderGuard.assertSuccess(result);
|
|
|
+ order = result;
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const [variant1, variant2, variant3, variant4] = product!.variants;
|
|
|
+
|
|
|
+ expect(variant1.stockAllocated).toBe(3);
|
|
|
+ expect(variant1.stockOnHand).toBe(3);
|
|
|
+
|
|
|
+ expect(variant2.stockAllocated).toBe(0); // inventory not tracked
|
|
|
+ expect(variant2.stockOnHand).toBe(3);
|
|
|
+
|
|
|
+ expect(variant3.stockAllocated).toBe(1);
|
|
|
+ expect(variant3.stockOnHand).toBe(3);
|
|
|
+
|
|
|
+ expect(variant4.stockAllocated).toBe(8);
|
|
|
+ expect(variant4.stockOnHand).toBe(3);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('addFulfillmentToOrder returns ErrorResult when insufficient stock on hand', async () => {
|
|
|
+ const { addFulfillmentToOrder } = await adminClient.query<
|
|
|
+ CreateFulfillment.Mutation,
|
|
|
+ CreateFulfillment.Variables
|
|
|
+ >(CREATE_FULFILLMENT, {
|
|
|
+ input: {
|
|
|
+ lines: order.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })),
|
|
|
+ method: 'test method',
|
|
|
+ trackingCode: 'ABC123',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ fulfillmentGuard.assertErrorResult(addFulfillmentToOrder);
|
|
|
+
|
|
|
+ expect(addFulfillmentToOrder.errorCode).toBe(AdminErrorCode.INSUFFICIENT_STOCK_ON_HAND_ERROR);
|
|
|
+ expect(addFulfillmentToOrder.message).toBe(
|
|
|
+ `Cannot create a Fulfillment as 'Laptop 15 inch 16GB' has insufficient stockOnHand (3)`,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it('addFulfillmentToOrder succeeds when there is sufficient stockOnHand', async () => {
|
|
|
+ const { addFulfillmentToOrder } = await adminClient.query<
|
|
|
+ CreateFulfillment.Mutation,
|
|
|
+ CreateFulfillment.Variables
|
|
|
+ >(CREATE_FULFILLMENT, {
|
|
|
+ input: {
|
|
|
+ lines: order.lines
|
|
|
+ .filter(l => l.productVariant.id === 'T_1')
|
|
|
+ .map(l => ({ orderLineId: l.id, quantity: l.quantity })),
|
|
|
+ method: 'test method',
|
|
|
+ trackingCode: 'ABC123',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[0];
|
|
|
+
|
|
|
+ expect(variant.stockOnHand).toBe(0);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('addFulfillmentToOrder succeeds when inventory is not being tracked', async () => {
|
|
|
+ const { addFulfillmentToOrder } = await adminClient.query<
|
|
|
+ CreateFulfillment.Mutation,
|
|
|
+ CreateFulfillment.Variables
|
|
|
+ >(CREATE_FULFILLMENT, {
|
|
|
+ input: {
|
|
|
+ lines: order.lines
|
|
|
+ .filter(l => l.productVariant.id === 'T_2')
|
|
|
+ .map(l => ({ orderLineId: l.id, quantity: l.quantity })),
|
|
|
+ method: 'test method',
|
|
|
+ trackingCode: 'ABC123',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[1];
|
|
|
+
|
|
|
+ expect(variant.stockOnHand).toBe(3);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('addFulfillmentToOrder succeeds when making a partial Fulfillment with quantity equal to stockOnHand', async () => {
|
|
|
+ const { addFulfillmentToOrder } = await adminClient.query<
|
|
|
+ CreateFulfillment.Mutation,
|
|
|
+ CreateFulfillment.Variables
|
|
|
+ >(CREATE_FULFILLMENT, {
|
|
|
+ input: {
|
|
|
+ lines: order.lines
|
|
|
+ .filter(l => l.productVariant.id === 'T_4')
|
|
|
+ .map(l => ({ orderLineId: l.id, quantity: 3 })), // we know there are only 3 on hand
|
|
|
+ method: 'test method',
|
|
|
+ trackingCode: 'ABC123',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[3];
|
|
|
+
|
|
|
+ expect(variant.stockOnHand).toBe(0);
|
|
|
+ expect(variant.stockAllocated).toBe(5);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('fulfillment can be created after adjusting stockOnHand to be sufficient', async () => {
|
|
|
+ const { updateProductVariants } = await adminClient.query<
|
|
|
+ UpdateProductVariants.Mutation,
|
|
|
+ UpdateProductVariants.Variables
|
|
|
+ >(UPDATE_PRODUCT_VARIANTS, {
|
|
|
+ input: [
|
|
|
+ {
|
|
|
+ id: 'T_4',
|
|
|
+ stockOnHand: 10,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(updateProductVariants[0]!.stockOnHand).toBe(10);
|
|
|
+
|
|
|
+ const { addFulfillmentToOrder } = await adminClient.query<
|
|
|
+ CreateFulfillment.Mutation,
|
|
|
+ CreateFulfillment.Variables
|
|
|
+ >(CREATE_FULFILLMENT, {
|
|
|
+ input: {
|
|
|
+ lines: order.lines
|
|
|
+ .filter(l => l.productVariant.id === 'T_4')
|
|
|
+ .map(l => ({ orderLineId: l.id, quantity: 5 })),
|
|
|
+ method: 'test method',
|
|
|
+ trackingCode: 'ABC123',
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ fulfillmentGuard.assertSuccess(addFulfillmentToOrder);
|
|
|
+
|
|
|
+ const product = await getProductWithStockMovement('T_1');
|
|
|
+ const variant = product!.variants[3];
|
|
|
+
|
|
|
+ expect(variant.stockOnHand).toBe(5);
|
|
|
+ expect(variant.stockAllocated).toBe(0);
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('edge cases', () => {
|
|
|
+ const variantId = 'T_5';
|
|
|
+
|
|
|
+ beforeAll(async () => {
|
|
|
+ // First place an order which creates a backorder (excess of allocated units)
|
|
|
+ await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
|
|
|
+ UPDATE_PRODUCT_VARIANTS,
|
|
|
+ {
|
|
|
+ input: [
|
|
|
+ {
|
|
|
+ id: variantId,
|
|
|
+ stockOnHand: 5,
|
|
|
+ outOfStockThreshold: -20,
|
|
|
+ trackInventory: GlobalFlag.TRUE,
|
|
|
+ useGlobalOutOfStockThreshold: false,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ );
|
|
|
+ await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
|
|
|
+ const { addItemToOrder: add1 } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 25,
|
|
|
+ });
|
|
|
+ orderGuard.assertSuccess(add1);
|
|
|
+ await proceedToArrangingPayment(shopClient);
|
|
|
+ await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('zero saleable stock', async () => {
|
|
|
+ await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
|
|
|
+ // The saleable stock level is now 0 (25 allocated, 5 on hand, -20 threshold)
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 1,
|
|
|
+ });
|
|
|
+ orderGuard.assertErrorResult(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
|
|
|
+ expect(addItemToOrder.message).toBe(
|
|
|
+ `No items were added to the order due to insufficient stock`,
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ it('negative saleable stock', async () => {
|
|
|
+ await adminClient.query<UpdateProductVariants.Mutation, UpdateProductVariants.Variables>(
|
|
|
+ UPDATE_PRODUCT_VARIANTS,
|
|
|
+ {
|
|
|
+ input: [
|
|
|
+ {
|
|
|
+ id: variantId,
|
|
|
+ outOfStockThreshold: -10,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ );
|
|
|
+ // The saleable stock level is now -10 (25 allocated, 5 on hand, -10 threshold)
|
|
|
+ await shopClient.asUserWithCredentials('marques.sawayn@hotmail.com', 'test');
|
|
|
+ const { addItemToOrder } = await shopClient.query<
|
|
|
+ AddItemToOrder.Mutation,
|
|
|
+ AddItemToOrder.Variables
|
|
|
+ >(ADD_ITEM_TO_ORDER, {
|
|
|
+ productVariantId: variantId,
|
|
|
+ quantity: 1,
|
|
|
+ });
|
|
|
+ orderGuard.assertErrorResult(addItemToOrder);
|
|
|
+
|
|
|
+ expect(addItemToOrder.errorCode).toBe(ErrorCode.INSUFFICIENT_STOCK_ERROR);
|
|
|
+ expect(addItemToOrder.message).toBe(
|
|
|
+ `No items were added to the order due to insufficient stock`,
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
const UPDATE_STOCK_ON_HAND = gql`
|