stock-location.e2e-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { CurrencyCode, DeletionResult, LanguageCode, SortOrder } from '@vendure/common/lib/generated-types';
  2. import { mergeConfig } from '@vendure/core';
  3. import {
  4. createErrorResultGuard,
  5. createTestEnvironment,
  6. E2E_DEFAULT_CHANNEL_TOKEN,
  7. ErrorResultGuard,
  8. } from '@vendure/testing';
  9. import path from 'path';
  10. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  11. import { initialData } from '../../../e2e-common/e2e-initial-data';
  12. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  13. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  14. import {
  15. testAssignStockLocationToChannelDocument,
  16. testCreateStockLocationDocument,
  17. testDeleteStockLocationDocument,
  18. testGetStockLevelsForVariantDocument,
  19. testGetStockLocationsListDocument,
  20. testRemoveStockLocationsFromChannelDocument,
  21. testSetStockLevelInLocationDocument,
  22. testUpdateStockLocationDocument,
  23. } from './graphql/admin-definitions';
  24. import { channelFragment } from './graphql/fragments-admin';
  25. import { FragmentOf } from './graphql/graphql-admin';
  26. import { FragmentOf as ShopFragmentOf } from './graphql/graphql-shop';
  27. import { assignProductToChannelDocument, createChannelDocument } from './graphql/shared-definitions';
  28. import { localUpdatedOrderFragment, testOrderFragment } from './graphql/shop-definitions';
  29. describe('Stock location', () => {
  30. const defaultStockLocationId = 'T_1';
  31. let secondStockLocationId: string;
  32. const { server, adminClient, shopClient } = createTestEnvironment(
  33. mergeConfig(testConfig(), {
  34. paymentOptions: {
  35. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  36. },
  37. }),
  38. );
  39. const orderGuard: ErrorResultGuard<
  40. ShopFragmentOf<typeof testOrderFragment> | ShopFragmentOf<typeof localUpdatedOrderFragment>
  41. > = createErrorResultGuard(input => !!input.lines);
  42. beforeAll(async () => {
  43. await server.init({
  44. initialData,
  45. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control-multi.csv'),
  46. customerCount: 3,
  47. });
  48. await adminClient.asSuperAdmin();
  49. }, TEST_SETUP_TIMEOUT_MS);
  50. afterAll(async () => {
  51. await server.destroy();
  52. });
  53. it('createStockLocation', async () => {
  54. const { createStockLocation } = await adminClient.query(testCreateStockLocationDocument, {
  55. input: {
  56. name: 'Second location',
  57. description: 'Second location description',
  58. },
  59. });
  60. expect(createStockLocation.name).toBe('Second location');
  61. expect(createStockLocation.description).toBe('Second location description');
  62. secondStockLocationId = createStockLocation.id;
  63. });
  64. it('updateStockLocation', async () => {
  65. const { updateStockLocation } = await adminClient.query(testUpdateStockLocationDocument, {
  66. input: {
  67. id: secondStockLocationId,
  68. name: 'Second location updated',
  69. description: 'Second location description updated',
  70. },
  71. });
  72. expect(updateStockLocation.name).toBe('Second location updated');
  73. expect(updateStockLocation.description).toBe('Second location description updated');
  74. });
  75. it('get stock locations list', async () => {
  76. const { stockLocations } = await adminClient.query(testGetStockLocationsListDocument, {
  77. options: {
  78. sort: {
  79. id: SortOrder.ASC,
  80. },
  81. },
  82. });
  83. expect(stockLocations.items.length).toBe(2);
  84. expect(stockLocations.items[0].name).toBe('Default Stock Location');
  85. expect(stockLocations.items[1].name).toBe('Second location updated');
  86. });
  87. it('assign stock to second location', async () => {
  88. const { updateProductVariants } = await adminClient.query(testSetStockLevelInLocationDocument, {
  89. input: {
  90. id: 'T_1',
  91. stockLevels: [
  92. {
  93. stockLocationId: secondStockLocationId,
  94. stockOnHand: 50,
  95. },
  96. ],
  97. },
  98. });
  99. expect(
  100. updateProductVariants[0]?.stockLevels.find(sl => sl.stockLocationId === defaultStockLocationId),
  101. ).toEqual({
  102. stockOnHand: 100,
  103. stockAllocated: 0,
  104. stockLocationId: defaultStockLocationId,
  105. });
  106. expect(
  107. updateProductVariants[0]?.stockLevels.find(sl => sl.stockLocationId === secondStockLocationId),
  108. ).toEqual({
  109. stockOnHand: 50,
  110. stockAllocated: 0,
  111. stockLocationId: secondStockLocationId,
  112. });
  113. });
  114. it('delete second stock location and assign stock to default location', async () => {
  115. const { deleteStockLocation } = await adminClient.query(testDeleteStockLocationDocument, {
  116. input: {
  117. id: secondStockLocationId,
  118. transferToLocationId: defaultStockLocationId,
  119. },
  120. });
  121. expect(deleteStockLocation.result).toBe(DeletionResult.DELETED);
  122. const { productVariant } = await adminClient.query(testGetStockLevelsForVariantDocument, {
  123. id: 'T_1',
  124. });
  125. expect(productVariant?.stockLevels.length).toBe(1);
  126. expect(productVariant?.stockLevels[0]).toEqual({
  127. stockOnHand: 150,
  128. stockAllocated: 0,
  129. stockLocationId: defaultStockLocationId,
  130. });
  131. });
  132. it('cannot delete last remaining stock location', async () => {
  133. const { deleteStockLocation } = await adminClient.query(testDeleteStockLocationDocument, {
  134. input: {
  135. id: defaultStockLocationId,
  136. },
  137. });
  138. expect(deleteStockLocation.result).toBe(DeletionResult.NOT_DELETED);
  139. expect(deleteStockLocation.message).toBe('The last remaining StockLocation cannot be deleted');
  140. const { stockLocations } = await adminClient.query(testGetStockLocationsListDocument);
  141. expect(stockLocations.items.length).toBe(1);
  142. });
  143. describe('multi channel', () => {
  144. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  145. let channelStockLocationId: string;
  146. let secondChannelId: string;
  147. const channelGuard: ErrorResultGuard<FragmentOf<typeof channelFragment>> = createErrorResultGuard<
  148. FragmentOf<typeof channelFragment>
  149. >(input => !!input.defaultLanguageCode);
  150. beforeAll(async () => {
  151. const { createStockLocation } = await adminClient.query(testCreateStockLocationDocument, {
  152. input: {
  153. name: 'Channel location',
  154. },
  155. });
  156. channelStockLocationId = createStockLocation.id;
  157. const { createChannel } = await adminClient.query(createChannelDocument, {
  158. input: {
  159. code: 'second-channel',
  160. token: SECOND_CHANNEL_TOKEN,
  161. defaultLanguageCode: LanguageCode.en,
  162. currencyCode: CurrencyCode.GBP,
  163. pricesIncludeTax: true,
  164. defaultShippingZoneId: 'T_1',
  165. defaultTaxZoneId: 'T_1',
  166. },
  167. });
  168. channelGuard.assertSuccess(createChannel);
  169. secondChannelId = createChannel.id;
  170. await adminClient.query(assignProductToChannelDocument, {
  171. input: {
  172. channelId: secondChannelId,
  173. productIds: ['T_1'],
  174. },
  175. });
  176. });
  177. it('stock location not visible in channel before being assigned', async () => {
  178. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  179. const { stockLocations } = await adminClient.query(testGetStockLocationsListDocument);
  180. expect(stockLocations.items.length).toBe(0);
  181. });
  182. it('assign stock location to channel', async () => {
  183. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  184. const { assignStockLocationsToChannel } = await adminClient.query(
  185. testAssignStockLocationToChannelDocument,
  186. {
  187. input: {
  188. stockLocationIds: [channelStockLocationId],
  189. channelId: secondChannelId,
  190. },
  191. },
  192. );
  193. expect(assignStockLocationsToChannel.length).toBe(1);
  194. expect(assignStockLocationsToChannel[0].name).toBe('Channel location');
  195. });
  196. it('stock location visible in channel once assigned', async () => {
  197. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  198. const { stockLocations } = await adminClient.query(testGetStockLocationsListDocument);
  199. expect(stockLocations.items.length).toBe(1);
  200. expect(stockLocations.items[0].name).toBe('Channel location');
  201. });
  202. it('assign stock to location in channel', async () => {
  203. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  204. const { updateProductVariants } = await adminClient.query(testSetStockLevelInLocationDocument, {
  205. input: {
  206. id: 'T_1',
  207. stockLevels: [
  208. {
  209. stockLocationId: channelStockLocationId,
  210. stockOnHand: 10,
  211. },
  212. ],
  213. },
  214. });
  215. });
  216. it('assigned variant stock level visible in channel', async () => {
  217. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  218. const { productVariant } = await adminClient.query(testGetStockLevelsForVariantDocument, {
  219. id: 'T_1',
  220. });
  221. expect(productVariant?.stockLevels.length).toBe(1);
  222. expect(productVariant?.stockLevels[0]).toEqual({
  223. stockOnHand: 10,
  224. stockAllocated: 0,
  225. stockLocationId: channelStockLocationId,
  226. });
  227. });
  228. it('remove stock location from channel', async () => {
  229. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  230. const { removeStockLocationsFromChannel } = await adminClient.query(
  231. testRemoveStockLocationsFromChannelDocument,
  232. {
  233. input: {
  234. stockLocationIds: [channelStockLocationId],
  235. channelId: secondChannelId,
  236. },
  237. },
  238. );
  239. expect(removeStockLocationsFromChannel.length).toBe(1);
  240. expect(removeStockLocationsFromChannel[0].name).toBe('Channel location');
  241. });
  242. it('variant stock level no longer visible once removed from channel', async () => {
  243. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  244. const { productVariant } = await adminClient.query(testGetStockLevelsForVariantDocument, {
  245. id: 'T_1',
  246. });
  247. expect(productVariant?.stockLevels.length).toBe(0);
  248. });
  249. });
  250. });