stock-control.e2e-spec.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /* tslint:disable:no-non-null-assertion */
  2. import { mergeConfig, OrderState } from '@vendure/core';
  3. import { createTestEnvironment } from '@vendure/testing';
  4. import gql from 'graphql-tag';
  5. import path from 'path';
  6. import { initialData } from '../../../e2e-common/e2e-initial-data';
  7. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  8. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  9. import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
  10. import {
  11. CreateAddressInput,
  12. GetStockMovement,
  13. StockMovementType,
  14. UpdateProductVariantInput,
  15. UpdateStock,
  16. VariantWithStockFragment,
  17. } from './graphql/generated-e2e-admin-types';
  18. import {
  19. AddItemToOrder,
  20. AddPaymentToOrder,
  21. PaymentInput,
  22. SetShippingAddress,
  23. TransitionToState,
  24. } from './graphql/generated-e2e-shop-types';
  25. import { GET_STOCK_MOVEMENT } from './graphql/shared-definitions';
  26. import {
  27. ADD_ITEM_TO_ORDER,
  28. ADD_PAYMENT,
  29. SET_SHIPPING_ADDRESS,
  30. TRANSITION_TO_STATE,
  31. } from './graphql/shop-definitions';
  32. import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
  33. describe('Stock control', () => {
  34. const { server, adminClient, shopClient } = createTestEnvironment(
  35. mergeConfig(testConfig, {
  36. paymentOptions: {
  37. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  38. },
  39. }),
  40. );
  41. beforeAll(async () => {
  42. await server.init({
  43. dataDir: path.join(__dirname, '__data__'),
  44. initialData,
  45. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control.csv'),
  46. customerCount: 2,
  47. });
  48. await adminClient.asSuperAdmin();
  49. }, TEST_SETUP_TIMEOUT_MS);
  50. afterAll(async () => {
  51. await server.destroy();
  52. });
  53. describe('stock adjustments', () => {
  54. let variants: VariantWithStockFragment[];
  55. it('stockMovements are initially empty', async () => {
  56. const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  57. GET_STOCK_MOVEMENT,
  58. { id: 'T_1' },
  59. );
  60. variants = product!.variants;
  61. for (const variant of variants) {
  62. expect(variant.stockMovements.items).toEqual([]);
  63. expect(variant.stockMovements.totalItems).toEqual(0);
  64. }
  65. });
  66. it('updating ProductVariant with same stockOnHand does not create a StockMovement', async () => {
  67. const { updateProductVariants } = await adminClient.query<
  68. UpdateStock.Mutation,
  69. UpdateStock.Variables
  70. >(UPDATE_STOCK_ON_HAND, {
  71. input: [
  72. {
  73. id: variants[0].id,
  74. stockOnHand: variants[0].stockOnHand,
  75. },
  76. ] as UpdateProductVariantInput[],
  77. });
  78. expect(updateProductVariants[0]!.stockMovements.items).toEqual([]);
  79. expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(0);
  80. });
  81. it('increasing stockOnHand creates a StockMovement with correct quantity', async () => {
  82. const { updateProductVariants } = await adminClient.query<
  83. UpdateStock.Mutation,
  84. UpdateStock.Variables
  85. >(UPDATE_STOCK_ON_HAND, {
  86. input: [
  87. {
  88. id: variants[0].id,
  89. stockOnHand: variants[0].stockOnHand + 5,
  90. },
  91. ] as UpdateProductVariantInput[],
  92. });
  93. expect(updateProductVariants[0]!.stockOnHand).toBe(5);
  94. expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(1);
  95. expect(updateProductVariants[0]!.stockMovements.items[0].type).toBe(StockMovementType.ADJUSTMENT);
  96. expect(updateProductVariants[0]!.stockMovements.items[0].quantity).toBe(5);
  97. });
  98. it('decreasing stockOnHand creates a StockMovement with correct quantity', async () => {
  99. const { updateProductVariants } = await adminClient.query<
  100. UpdateStock.Mutation,
  101. UpdateStock.Variables
  102. >(UPDATE_STOCK_ON_HAND, {
  103. input: [
  104. {
  105. id: variants[0].id,
  106. stockOnHand: variants[0].stockOnHand + 5 - 2,
  107. },
  108. ] as UpdateProductVariantInput[],
  109. });
  110. expect(updateProductVariants[0]!.stockOnHand).toBe(3);
  111. expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(2);
  112. expect(updateProductVariants[0]!.stockMovements.items[1].type).toBe(StockMovementType.ADJUSTMENT);
  113. expect(updateProductVariants[0]!.stockMovements.items[1].quantity).toBe(-2);
  114. });
  115. it(
  116. 'attempting to set a negative stockOnHand throws',
  117. assertThrowsWithMessage(async () => {
  118. const result = await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(
  119. UPDATE_STOCK_ON_HAND,
  120. {
  121. input: [
  122. {
  123. id: variants[0].id,
  124. stockOnHand: -1,
  125. },
  126. ] as UpdateProductVariantInput[],
  127. },
  128. );
  129. }, 'stockOnHand cannot be a negative value'),
  130. );
  131. });
  132. describe('sales', () => {
  133. beforeAll(async () => {
  134. const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  135. GET_STOCK_MOVEMENT,
  136. { id: 'T_2' },
  137. );
  138. const [variant1, variant2] = product!.variants;
  139. await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(UPDATE_STOCK_ON_HAND, {
  140. input: [
  141. {
  142. id: variant1.id,
  143. stockOnHand: 5,
  144. trackInventory: false,
  145. },
  146. {
  147. id: variant2.id,
  148. stockOnHand: 5,
  149. trackInventory: true,
  150. },
  151. ] as UpdateProductVariantInput[],
  152. });
  153. // Add items to order and check out
  154. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  155. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  156. productVariantId: variant1.id,
  157. quantity: 2,
  158. });
  159. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  160. productVariantId: variant2.id,
  161. quantity: 3,
  162. });
  163. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  164. SET_SHIPPING_ADDRESS,
  165. {
  166. input: {
  167. streetLine1: '1 Test Street',
  168. countryCode: 'GB',
  169. } as CreateAddressInput,
  170. },
  171. );
  172. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
  173. TRANSITION_TO_STATE,
  174. { state: 'ArrangingPayment' as OrderState },
  175. );
  176. });
  177. it('creates a Sale when order completed', async () => {
  178. const { addPaymentToOrder } = await shopClient.query<
  179. AddPaymentToOrder.Mutation,
  180. AddPaymentToOrder.Variables
  181. >(ADD_PAYMENT, {
  182. input: {
  183. method: testSuccessfulPaymentMethod.code,
  184. metadata: {},
  185. } as PaymentInput,
  186. });
  187. expect(addPaymentToOrder).not.toBeNull();
  188. const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  189. GET_STOCK_MOVEMENT,
  190. { id: 'T_2' },
  191. );
  192. const [variant1, variant2] = product!.variants;
  193. expect(variant1.stockMovements.totalItems).toBe(2);
  194. expect(variant1.stockMovements.items[1].type).toBe(StockMovementType.SALE);
  195. expect(variant1.stockMovements.items[1].quantity).toBe(-2);
  196. expect(variant2.stockMovements.totalItems).toBe(2);
  197. expect(variant2.stockMovements.items[1].type).toBe(StockMovementType.SALE);
  198. expect(variant2.stockMovements.items[1].quantity).toBe(-3);
  199. });
  200. it('stockOnHand is updated according to trackInventory setting', async () => {
  201. const { product } = await adminClient.query<GetStockMovement.Query, GetStockMovement.Variables>(
  202. GET_STOCK_MOVEMENT,
  203. { id: 'T_2' },
  204. );
  205. const [variant1, variant2] = product!.variants;
  206. expect(variant1.stockOnHand).toBe(5); // untracked inventory
  207. expect(variant2.stockOnHand).toBe(2); // tracked inventory
  208. });
  209. });
  210. });
  211. const UPDATE_STOCK_ON_HAND = gql`
  212. mutation UpdateStock($input: [UpdateProductVariantInput!]!) {
  213. updateProductVariants(input: $input) {
  214. ...VariantWithStock
  215. }
  216. }
  217. ${VARIANT_WITH_STOCK_FRAGMENT}
  218. `;