fulfillment-process.e2e-spec.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { ErrorCode } from '@vendure/common/lib/generated-types';
  3. import {
  4. CustomFulfillmentProcess,
  5. defaultFulfillmentProcess,
  6. manualFulfillmentHandler,
  7. mergeConfig,
  8. TransactionalConnection,
  9. } from '@vendure/core';
  10. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  11. import path from 'path';
  12. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  13. import { initialData } from '../../../e2e-common/e2e-initial-data';
  14. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  15. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  16. import { fulfillmentFragment } from './graphql/fragments-admin';
  17. import { FragmentOf } from './graphql/graphql-admin';
  18. import {
  19. createFulfillmentDocument,
  20. getCustomerListDocument,
  21. getOrderFulfillmentsDocument,
  22. transitFulfillmentDocument,
  23. } from './graphql/shared-definitions';
  24. import { addItemToOrderDocument } from './graphql/shop-definitions';
  25. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  26. const initSpy = vi.fn();
  27. const transitionStartSpy = vi.fn();
  28. const transitionEndSpy = vi.fn();
  29. const transitionEndSpy2 = vi.fn();
  30. const transitionErrorSpy = vi.fn();
  31. describe('Fulfillment process', () => {
  32. type FulfillmentFragment = FragmentOf<typeof fulfillmentFragment>;
  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(getCustomerListDocument, {
  103. options: {
  104. take: 3,
  105. },
  106. });
  107. const customers = result.customers.items;
  108. /**
  109. * Creates a Orders to test Fulfillment Process
  110. */
  111. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  112. // Add Items
  113. await shopClient.query(addItemToOrderDocument, {
  114. productVariantId: 'T_1',
  115. quantity: 1,
  116. });
  117. await shopClient.query(addItemToOrderDocument, {
  118. productVariantId: 'T_2',
  119. quantity: 1,
  120. });
  121. // Transit to payment
  122. await proceedToArrangingPayment(shopClient);
  123. await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  124. // Add a fulfillment without tracking code
  125. await adminClient.query(createFulfillmentDocument, {
  126. input: {
  127. lines: [{ orderLineId: 'T_1', quantity: 1 }],
  128. handler: {
  129. code: manualFulfillmentHandler.code,
  130. arguments: [{ name: 'method', value: 'Test1' }],
  131. },
  132. },
  133. });
  134. // Add a fulfillment with tracking code
  135. await adminClient.query(createFulfillmentDocument, {
  136. input: {
  137. lines: [{ orderLineId: 'T_2', quantity: 1 }],
  138. handler: {
  139. code: manualFulfillmentHandler.code,
  140. arguments: [
  141. { name: 'method', value: 'Test1' },
  142. { name: 'trackingCode', value: '222' },
  143. ],
  144. },
  145. },
  146. });
  147. }, TEST_SETUP_TIMEOUT_MS);
  148. afterAll(async () => {
  149. await server.destroy();
  150. });
  151. describe('CustomFulfillmentProcess', () => {
  152. it('is injectable', () => {
  153. expect(initSpy).toHaveBeenCalled();
  154. expect(initSpy.mock.calls[0][0]).toBe('default');
  155. });
  156. it('replaced transition target', async () => {
  157. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  158. id: 'T_1',
  159. });
  160. const [fulfillment] = order?.fulfillments || [];
  161. expect(fulfillment.nextStates).toEqual(['AwaitingPickup']);
  162. });
  163. it('custom onTransitionStart handler returning error message', async () => {
  164. // First transit to AwaitingPickup
  165. await adminClient.query(transitFulfillmentDocument, {
  166. id: 'T_1',
  167. state: 'AwaitingPickup',
  168. });
  169. transitionStartSpy.mockClear();
  170. transitionErrorSpy.mockClear();
  171. transitionEndSpy.mockClear();
  172. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  173. id: 'T_1',
  174. state: 'Shipped',
  175. });
  176. fulfillmentGuard.assertErrorResult(transitionFulfillmentToState);
  177. expect(transitionFulfillmentToState.errorCode).toBe(ErrorCode.FULFILLMENT_STATE_TRANSITION_ERROR);
  178. expect(transitionFulfillmentToState.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  179. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  180. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  181. expect(transitionEndSpy).not.toHaveBeenCalled();
  182. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  183. 'AwaitingPickup',
  184. 'Shipped',
  185. VALIDATION_ERROR_MESSAGE,
  186. ]);
  187. });
  188. it('custom onTransitionStart handler allows transition', async () => {
  189. transitionEndSpy.mockClear();
  190. // First transit to AwaitingPickup
  191. await adminClient.query(transitFulfillmentDocument, {
  192. id: 'T_2',
  193. state: 'AwaitingPickup',
  194. });
  195. transitionEndSpy.mockClear();
  196. const { transitionFulfillmentToState } = await adminClient.query(transitFulfillmentDocument, {
  197. id: 'T_2',
  198. state: 'Shipped',
  199. });
  200. fulfillmentGuard.assertSuccess(transitionFulfillmentToState);
  201. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  202. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AwaitingPickup', 'Shipped']);
  203. expect(transitionFulfillmentToState?.state).toBe('Shipped');
  204. });
  205. it('composes multiple CustomFulfillmentProcesses', async () => {
  206. const { order } = await adminClient.query(getOrderFulfillmentsDocument, {
  207. id: 'T_1',
  208. });
  209. const [fulfillment] = order?.fulfillments || [];
  210. expect(fulfillment.nextStates).toEqual(['Shipped', 'Cancelled']);
  211. });
  212. });
  213. });