stock-control.e2e-spec.ts 9.8 KB

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