stock-control.e2e-spec.ts 9.2 KB

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