fulfillment-process.e2e-spec.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. CustomFulfillmentProcess,
  4. defaultFulfillmentProcess,
  5. manualFulfillmentHandler,
  6. mergeConfig,
  7. TransactionalConnection,
  8. } from '@vendure/core';
  9. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  10. import path from 'path';
  11. import { vi } from 'vitest';
  12. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  13. import { initialData } from '../../../e2e-common/e2e-initial-data';
  14. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  15. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  16. import { ErrorCode, FulfillmentFragment } from './graphql/generated-e2e-admin-types';
  17. import * as Codegen from './graphql/generated-e2e-admin-types';
  18. import { AddItemToOrderMutation, AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-types';
  19. import {
  20. CREATE_FULFILLMENT,
  21. GET_CUSTOMER_LIST,
  22. GET_ORDER_FULFILLMENTS,
  23. TRANSIT_FULFILLMENT,
  24. } from './graphql/shared-definitions';
  25. import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
  26. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  27. const initSpy = vi.fn();
  28. const transitionStartSpy = vi.fn();
  29. const transitionEndSpy = vi.fn();
  30. const transitionEndSpy2 = vi.fn();
  31. const transitionErrorSpy = vi.fn();
  32. describe('Fulfillment process', () => {
  33. const fulfillmentGuard: ErrorResultGuard<FulfillmentFragment> = createErrorResultGuard(
  34. input => !!input.id,
  35. );
  36. const VALIDATION_ERROR_MESSAGE = 'Fulfillment must have a tracking code';
  37. const customOrderProcess: CustomFulfillmentProcess<'AwaitingPickup'> = {
  38. init(injector) {
  39. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  40. },
  41. transitions: {
  42. Pending: {
  43. to: ['AwaitingPickup'],
  44. mergeStrategy: 'replace',
  45. },
  46. AwaitingPickup: {
  47. to: ['Shipped'],
  48. },
  49. },
  50. onTransitionStart(fromState, toState, data) {
  51. transitionStartSpy(fromState, toState, data);
  52. if (fromState === 'AwaitingPickup' && toState === 'Shipped') {
  53. if (!data.fulfillment.trackingCode) {
  54. return VALIDATION_ERROR_MESSAGE;
  55. }
  56. }
  57. },
  58. onTransitionEnd(fromState, toState, data) {
  59. transitionEndSpy(fromState, toState, data);
  60. },
  61. onTransitionError(fromState, toState, message) {
  62. transitionErrorSpy(fromState, toState, message);
  63. },
  64. };
  65. const customOrderProcess2: CustomFulfillmentProcess<'AwaitingPickup'> = {
  66. transitions: {
  67. AwaitingPickup: {
  68. to: ['Cancelled'],
  69. },
  70. },
  71. onTransitionEnd(fromState, toState, data) {
  72. transitionEndSpy2(fromState, toState, data);
  73. },
  74. };
  75. const { server, adminClient, shopClient } = createTestEnvironment(
  76. mergeConfig(testConfig(), {
  77. shippingOptions: {
  78. ...testConfig().shippingOptions,
  79. process: [defaultFulfillmentProcess, customOrderProcess as any, customOrderProcess2 as any],
  80. },
  81. paymentOptions: {
  82. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  83. },
  84. }),
  85. );
  86. beforeAll(async () => {
  87. await server.init({
  88. initialData: {
  89. ...initialData,
  90. paymentMethods: [
  91. {
  92. name: testSuccessfulPaymentMethod.code,
  93. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  94. },
  95. ],
  96. },
  97. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  98. customerCount: 1,
  99. });
  100. await adminClient.asSuperAdmin();
  101. // Create a couple of orders to be queried
  102. const result = await adminClient.query<
  103. Codegen.GetCustomerListQuery,
  104. Codegen.GetCustomerListQueryVariables
  105. >(GET_CUSTOMER_LIST, {
  106. options: {
  107. take: 3,
  108. },
  109. });
  110. const customers = result.customers.items;
  111. /**
  112. * Creates a Orders to test Fulfillment Process
  113. */
  114. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  115. // Add Items
  116. await shopClient.query<AddItemToOrderMutation, AddItemToOrderMutationVariables>(ADD_ITEM_TO_ORDER, {
  117. productVariantId: 'T_1',
  118. quantity: 1,
  119. });
  120. await shopClient.query<AddItemToOrderMutation, AddItemToOrderMutationVariables>(ADD_ITEM_TO_ORDER, {
  121. productVariantId: 'T_2',
  122. quantity: 1,
  123. });
  124. // Transit to payment
  125. await proceedToArrangingPayment(shopClient);
  126. await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  127. // Add a fulfillment without tracking code
  128. await adminClient.query<
  129. Codegen.CreateFulfillmentMutation,
  130. Codegen.CreateFulfillmentMutationVariables
  131. >(CREATE_FULFILLMENT, {
  132. input: {
  133. lines: [{ orderLineId: 'T_1', quantity: 1 }],
  134. handler: {
  135. code: manualFulfillmentHandler.code,
  136. arguments: [{ name: 'method', value: 'Test1' }],
  137. },
  138. },
  139. });
  140. // Add a fulfillment with tracking code
  141. await adminClient.query<
  142. Codegen.CreateFulfillmentMutation,
  143. Codegen.CreateFulfillmentMutationVariables
  144. >(CREATE_FULFILLMENT, {
  145. input: {
  146. lines: [{ orderLineId: 'T_2', quantity: 1 }],
  147. handler: {
  148. code: manualFulfillmentHandler.code,
  149. arguments: [
  150. { name: 'method', value: 'Test1' },
  151. { name: 'trackingCode', value: '222' },
  152. ],
  153. },
  154. },
  155. });
  156. }, TEST_SETUP_TIMEOUT_MS);
  157. afterAll(async () => {
  158. await server.destroy();
  159. });
  160. describe('CustomFulfillmentProcess', () => {
  161. it('is injectable', () => {
  162. expect(initSpy).toHaveBeenCalled();
  163. expect(initSpy.mock.calls[0][0]).toBe('default');
  164. });
  165. it('replaced transition target', async () => {
  166. const { order } = await adminClient.query<
  167. Codegen.GetOrderFulfillmentsQuery,
  168. Codegen.GetOrderFulfillmentsQueryVariables
  169. >(GET_ORDER_FULFILLMENTS, {
  170. id: 'T_1',
  171. });
  172. const [fulfillment] = order?.fulfillments || [];
  173. expect(fulfillment.nextStates).toEqual(['AwaitingPickup']);
  174. });
  175. it('custom onTransitionStart handler returning error message', async () => {
  176. // First transit to AwaitingPickup
  177. await adminClient.query<
  178. Codegen.TransitFulfillmentMutation,
  179. Codegen.TransitFulfillmentMutationVariables
  180. >(TRANSIT_FULFILLMENT, {
  181. id: 'T_1',
  182. state: 'AwaitingPickup',
  183. });
  184. transitionStartSpy.mockClear();
  185. transitionErrorSpy.mockClear();
  186. transitionEndSpy.mockClear();
  187. const { transitionFulfillmentToState } = await adminClient.query<
  188. Codegen.TransitFulfillmentMutation,
  189. Codegen.TransitFulfillmentMutationVariables
  190. >(TRANSIT_FULFILLMENT, {
  191. id: 'T_1',
  192. state: 'Shipped',
  193. });
  194. fulfillmentGuard.assertErrorResult(transitionFulfillmentToState);
  195. expect(transitionFulfillmentToState.errorCode).toBe(ErrorCode.FULFILLMENT_STATE_TRANSITION_ERROR);
  196. expect(transitionFulfillmentToState.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  197. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  198. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  199. expect(transitionEndSpy).not.toHaveBeenCalled();
  200. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  201. 'AwaitingPickup',
  202. 'Shipped',
  203. VALIDATION_ERROR_MESSAGE,
  204. ]);
  205. });
  206. it('custom onTransitionStart handler allows transition', async () => {
  207. transitionEndSpy.mockClear();
  208. // First transit to AwaitingPickup
  209. await adminClient.query<
  210. Codegen.TransitFulfillmentMutation,
  211. Codegen.TransitFulfillmentMutationVariables
  212. >(TRANSIT_FULFILLMENT, {
  213. id: 'T_2',
  214. state: 'AwaitingPickup',
  215. });
  216. transitionEndSpy.mockClear();
  217. const { transitionFulfillmentToState } = await adminClient.query<
  218. Codegen.TransitFulfillmentMutation,
  219. Codegen.TransitFulfillmentMutationVariables
  220. >(TRANSIT_FULFILLMENT, {
  221. id: 'T_2',
  222. state: 'Shipped',
  223. });
  224. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  225. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  226. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AwaitingPickup', 'Shipped']);
  227. expect(transitionFulfillmentToState?.state).toBe('Shipped');
  228. });
  229. it('composes multiple CustomFulfillmentProcesses', async () => {
  230. const { order } = await adminClient.query<
  231. Codegen.GetOrderFulfillmentsQuery,
  232. Codegen.GetOrderFulfillmentsQueryVariables
  233. >(GET_ORDER_FULFILLMENTS, {
  234. id: 'T_1',
  235. });
  236. const [fulfillment] = order?.fulfillments || [];
  237. expect(fulfillment.nextStates).toEqual(['Shipped', 'Cancelled']);
  238. });
  239. });
  240. });