stock-location.e2e-spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import { mergeConfig } from '@vendure/core';
  2. import {
  3. createErrorResultGuard,
  4. createTestEnvironment,
  5. E2E_DEFAULT_CHANNEL_TOKEN,
  6. ErrorResultGuard,
  7. } from '@vendure/testing';
  8. import gql from 'graphql-tag';
  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 * as Codegen from './graphql/generated-e2e-admin-types';
  15. import {
  16. AssignProductsToChannelDocument,
  17. CurrencyCode,
  18. DeletionResult,
  19. LanguageCode,
  20. SortOrder,
  21. TestAssignStockLocationToChannelDocument,
  22. TestCreateStockLocationDocument,
  23. TestDeleteStockLocationDocument,
  24. TestGetStockLevelsForVariantDocument,
  25. TestGetStockLocationsListDocument,
  26. TestRemoveStockLocationsFromChannelDocument,
  27. TestSetStockLevelInLocationDocument,
  28. TestUpdateStockLocationDocument,
  29. } from './graphql/generated-e2e-admin-types';
  30. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  31. import { CREATE_CHANNEL } from './graphql/shared-definitions';
  32. describe('Stock location', () => {
  33. const defaultStockLocationId = 'T_1';
  34. let secondStockLocationId: string;
  35. const { server, adminClient, shopClient } = createTestEnvironment(
  36. mergeConfig(testConfig(), {
  37. paymentOptions: {
  38. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  39. },
  40. }),
  41. );
  42. const orderGuard: ErrorResultGuard<
  43. CodegenShop.TestOrderFragmentFragment | CodegenShop.UpdatedOrderFragment
  44. > = createErrorResultGuard(input => !!input.lines);
  45. beforeAll(async () => {
  46. await server.init({
  47. initialData,
  48. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-stock-control-multi.csv'),
  49. customerCount: 3,
  50. });
  51. await adminClient.asSuperAdmin();
  52. }, TEST_SETUP_TIMEOUT_MS);
  53. afterAll(async () => {
  54. await server.destroy();
  55. });
  56. it('createStockLocation', async () => {
  57. const { createStockLocation } = await adminClient.query(TestCreateStockLocationDocument, {
  58. input: {
  59. name: 'Second location',
  60. description: 'Second location description',
  61. },
  62. });
  63. expect(createStockLocation.name).toBe('Second location');
  64. expect(createStockLocation.description).toBe('Second location description');
  65. secondStockLocationId = createStockLocation.id;
  66. });
  67. it('updateStockLocation', async () => {
  68. const { updateStockLocation } = await adminClient.query(TestUpdateStockLocationDocument, {
  69. input: {
  70. id: secondStockLocationId,
  71. name: 'Second location updated',
  72. description: 'Second location description updated',
  73. },
  74. });
  75. expect(updateStockLocation.name).toBe('Second location updated');
  76. expect(updateStockLocation.description).toBe('Second location description updated');
  77. });
  78. it('get stock locations list', async () => {
  79. const { stockLocations } = await adminClient.query(TestGetStockLocationsListDocument, {
  80. options: {
  81. sort: {
  82. id: SortOrder.ASC,
  83. },
  84. },
  85. });
  86. expect(stockLocations.items.length).toBe(2);
  87. expect(stockLocations.items[0].name).toBe('Default Stock Location');
  88. expect(stockLocations.items[1].name).toBe('Second location updated');
  89. });
  90. it('assign stock to second location', async () => {
  91. const { updateProductVariants } = await adminClient.query(TestSetStockLevelInLocationDocument, {
  92. input: {
  93. id: 'T_1',
  94. stockLevels: [
  95. {
  96. stockLocationId: secondStockLocationId,
  97. stockOnHand: 50,
  98. },
  99. ],
  100. },
  101. });
  102. expect(
  103. updateProductVariants[0]?.stockLevels.find(sl => sl.stockLocationId === defaultStockLocationId),
  104. ).toEqual({
  105. stockOnHand: 100,
  106. stockAllocated: 0,
  107. stockLocationId: defaultStockLocationId,
  108. });
  109. expect(
  110. updateProductVariants[0]?.stockLevels.find(sl => sl.stockLocationId === secondStockLocationId),
  111. ).toEqual({
  112. stockOnHand: 50,
  113. stockAllocated: 0,
  114. stockLocationId: secondStockLocationId,
  115. });
  116. });
  117. it('delete second stock location and assign stock to default location', async () => {
  118. const { deleteStockLocation } = await adminClient.query(TestDeleteStockLocationDocument, {
  119. input: {
  120. id: secondStockLocationId,
  121. transferToLocationId: defaultStockLocationId,
  122. },
  123. });
  124. expect(deleteStockLocation.result).toBe(DeletionResult.DELETED);
  125. const { productVariant } = await adminClient.query(TestGetStockLevelsForVariantDocument, {
  126. id: 'T_1',
  127. });
  128. expect(productVariant?.stockLevels.length).toBe(1);
  129. expect(productVariant?.stockLevels[0]).toEqual({
  130. stockOnHand: 150,
  131. stockAllocated: 0,
  132. stockLocationId: defaultStockLocationId,
  133. });
  134. });
  135. it('cannot delete last remaining stock location', async () => {
  136. const { deleteStockLocation } = await adminClient.query(TestDeleteStockLocationDocument, {
  137. input: {
  138. id: defaultStockLocationId,
  139. },
  140. });
  141. expect(deleteStockLocation.result).toBe(DeletionResult.NOT_DELETED);
  142. expect(deleteStockLocation.message).toBe('The last remaining StockLocation cannot be deleted');
  143. const { stockLocations } = await adminClient.query(TestGetStockLocationsListDocument);
  144. expect(stockLocations.items.length).toBe(1);
  145. });
  146. describe('multi channel', () => {
  147. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  148. let channelStockLocationId: string;
  149. let secondChannelId: string;
  150. const channelGuard: ErrorResultGuard<Codegen.ChannelFragment> =
  151. createErrorResultGuard<Codegen.ChannelFragment>(input => !!input.defaultLanguageCode);
  152. beforeAll(async () => {
  153. const { createStockLocation } = await adminClient.query(TestCreateStockLocationDocument, {
  154. input: {
  155. name: 'Channel location',
  156. },
  157. });
  158. channelStockLocationId = createStockLocation.id;
  159. const { createChannel } = await adminClient.query<
  160. Codegen.CreateChannelMutation,
  161. Codegen.CreateChannelMutationVariables
  162. >(CREATE_CHANNEL, {
  163. input: {
  164. code: 'second-channel',
  165. token: SECOND_CHANNEL_TOKEN,
  166. defaultLanguageCode: LanguageCode.en,
  167. currencyCode: CurrencyCode.GBP,
  168. pricesIncludeTax: true,
  169. defaultShippingZoneId: 'T_1',
  170. defaultTaxZoneId: 'T_1',
  171. },
  172. });
  173. channelGuard.assertSuccess(createChannel);
  174. secondChannelId = createChannel.id;
  175. await adminClient.query(AssignProductsToChannelDocument, {
  176. input: {
  177. channelId: secondChannelId,
  178. productIds: ['T_1'],
  179. },
  180. });
  181. });
  182. it('stock location not visible in channel before being assigned', async () => {
  183. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  184. const { stockLocations } = await adminClient.query(TestGetStockLocationsListDocument);
  185. expect(stockLocations.items.length).toBe(0);
  186. });
  187. it('assign stock location to channel', async () => {
  188. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  189. const { assignStockLocationsToChannel } = await adminClient.query(
  190. TestAssignStockLocationToChannelDocument,
  191. {
  192. input: {
  193. stockLocationIds: [channelStockLocationId],
  194. channelId: secondChannelId,
  195. },
  196. },
  197. );
  198. expect(assignStockLocationsToChannel.length).toBe(1);
  199. expect(assignStockLocationsToChannel[0].name).toBe('Channel location');
  200. });
  201. it('stock location visible in channel once assigned', async () => {
  202. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  203. const { stockLocations } = await adminClient.query(TestGetStockLocationsListDocument);
  204. expect(stockLocations.items.length).toBe(1);
  205. expect(stockLocations.items[0].name).toBe('Channel location');
  206. });
  207. it('assign stock to location in channel', async () => {
  208. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  209. const { updateProductVariants } = await adminClient.query(TestSetStockLevelInLocationDocument, {
  210. input: {
  211. id: 'T_1',
  212. stockLevels: [
  213. {
  214. stockLocationId: channelStockLocationId,
  215. stockOnHand: 10,
  216. },
  217. ],
  218. },
  219. });
  220. });
  221. it('assigned variant stock level visible in channel', async () => {
  222. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  223. const { productVariant } = await adminClient.query(TestGetStockLevelsForVariantDocument, {
  224. id: 'T_1',
  225. });
  226. expect(productVariant?.stockLevels.length).toBe(1);
  227. expect(productVariant?.stockLevels[0]).toEqual({
  228. stockOnHand: 10,
  229. stockAllocated: 0,
  230. stockLocationId: channelStockLocationId,
  231. });
  232. });
  233. it('remove stock location from channel', async () => {
  234. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  235. const { removeStockLocationsFromChannel } = await adminClient.query(
  236. TestRemoveStockLocationsFromChannelDocument,
  237. {
  238. input: {
  239. stockLocationIds: [channelStockLocationId],
  240. channelId: secondChannelId,
  241. },
  242. },
  243. );
  244. expect(removeStockLocationsFromChannel.length).toBe(1);
  245. expect(removeStockLocationsFromChannel[0].name).toBe('Channel location');
  246. });
  247. it('variant stock level no longer visible once removed from channel', async () => {
  248. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  249. const { productVariant } = await adminClient.query(TestGetStockLevelsForVariantDocument, {
  250. id: 'T_1',
  251. });
  252. expect(productVariant?.stockLevels.length).toBe(0);
  253. });
  254. });
  255. });
  256. const GET_STOCK_LOCATIONS_LIST = gql`
  257. query TestGetStockLocationsList($options: StockLocationListOptions) {
  258. stockLocations(options: $options) {
  259. items {
  260. id
  261. name
  262. description
  263. }
  264. totalItems
  265. }
  266. }
  267. `;
  268. const GET_STOCK_LOCATION = gql`
  269. query TestGetStockLocation($id: ID!) {
  270. stockLocation(id: $id) {
  271. id
  272. name
  273. description
  274. }
  275. }
  276. `;
  277. const CREATE_STOCK_LOCATION = gql`
  278. mutation TestCreateStockLocation($input: CreateStockLocationInput!) {
  279. createStockLocation(input: $input) {
  280. id
  281. name
  282. description
  283. }
  284. }
  285. `;
  286. const UPDATE_STOCK_LOCATION = gql`
  287. mutation TestUpdateStockLocation($input: UpdateStockLocationInput!) {
  288. updateStockLocation(input: $input) {
  289. id
  290. name
  291. description
  292. }
  293. }
  294. `;
  295. const DELETE_STOCK_LOCATION = gql`
  296. mutation TestDeleteStockLocation($input: DeleteStockLocationInput!) {
  297. deleteStockLocation(input: $input) {
  298. result
  299. message
  300. }
  301. }
  302. `;
  303. const GET_STOCK_LEVELS_FOR_VARIANT = gql`
  304. query TestGetStockLevelsForVariant($id: ID!) {
  305. productVariant(id: $id) {
  306. id
  307. stockLevels {
  308. stockOnHand
  309. stockAllocated
  310. stockLocationId
  311. }
  312. }
  313. }
  314. `;
  315. const SET_STOCK_LEVEL_IN_LOCATION = gql`
  316. mutation TestSetStockLevelInLocation($input: UpdateProductVariantInput!) {
  317. updateProductVariants(input: [$input]) {
  318. id
  319. stockLevels {
  320. stockOnHand
  321. stockAllocated
  322. stockLocationId
  323. }
  324. }
  325. }
  326. `;
  327. const ASSIGN_STOCK_LOCATION_TO_CHANNEL = gql`
  328. mutation TestAssignStockLocationToChannel($input: AssignStockLocationsToChannelInput!) {
  329. assignStockLocationsToChannel(input: $input) {
  330. id
  331. name
  332. }
  333. }
  334. `;
  335. const REMOVE_STOCK_LOCATIONS_FROM_CHANNEL = gql`
  336. mutation TestRemoveStockLocationsFromChannel($input: RemoveStockLocationsFromChannelInput!) {
  337. removeStockLocationsFromChannel(input: $input) {
  338. id
  339. name
  340. }
  341. }
  342. `;