order-process.e2e-spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /* tslint:disable:no-non-null-assertion */
  2. import { CustomOrderProcess, mergeConfig, OrderState, TransactionalConnection } from '@vendure/core';
  3. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  4. import path from 'path';
  5. import { initialData } from '../../../e2e-common/e2e-initial-data';
  6. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  7. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  8. import { AdminTransition, GetOrder, OrderFragment } from './graphql/generated-e2e-admin-types';
  9. import {
  10. AddItemToOrder,
  11. AddPaymentToOrder,
  12. ErrorCode,
  13. GetNextOrderStates,
  14. SetCustomerForOrder,
  15. SetShippingAddress,
  16. SetShippingMethod,
  17. TestOrderFragmentFragment,
  18. TransitionToState,
  19. } from './graphql/generated-e2e-shop-types';
  20. import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
  21. import {
  22. ADD_ITEM_TO_ORDER,
  23. ADD_PAYMENT,
  24. GET_NEXT_STATES,
  25. SET_CUSTOMER,
  26. SET_SHIPPING_ADDRESS,
  27. SET_SHIPPING_METHOD,
  28. TRANSITION_TO_STATE,
  29. } from './graphql/shop-definitions';
  30. type TestOrderState = OrderState | 'ValidatingCustomer';
  31. const initSpy = jest.fn();
  32. const transitionStartSpy = jest.fn();
  33. const transitionEndSpy = jest.fn();
  34. const transitionEndSpy2 = jest.fn();
  35. const transitionErrorSpy = jest.fn();
  36. describe('Order process', () => {
  37. const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
  38. const customOrderProcess: CustomOrderProcess<'ValidatingCustomer'> = {
  39. init(injector) {
  40. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  41. },
  42. transitions: {
  43. AddingItems: {
  44. to: ['ValidatingCustomer'],
  45. mergeStrategy: 'replace',
  46. },
  47. ValidatingCustomer: {
  48. to: ['ArrangingPayment', 'AddingItems'],
  49. },
  50. },
  51. onTransitionStart(fromState, toState, data) {
  52. transitionStartSpy(fromState, toState, data);
  53. if (toState === 'ValidatingCustomer') {
  54. if (!data.order.customer) {
  55. return false;
  56. }
  57. if (!data.order.customer.emailAddress.includes('@company.com')) {
  58. return VALIDATION_ERROR_MESSAGE;
  59. }
  60. }
  61. },
  62. onTransitionEnd(fromState, toState, data) {
  63. transitionEndSpy(fromState, toState, data);
  64. },
  65. onTransitionError(fromState, toState, message) {
  66. transitionErrorSpy(fromState, toState, message);
  67. },
  68. };
  69. const customOrderProcess2: CustomOrderProcess<'ValidatingCustomer'> = {
  70. transitions: {
  71. ValidatingCustomer: {
  72. to: ['Cancelled'],
  73. },
  74. },
  75. onTransitionEnd(fromState, toState, data) {
  76. transitionEndSpy2(fromState, toState, data);
  77. },
  78. };
  79. const orderErrorGuard: ErrorResultGuard<TestOrderFragmentFragment | OrderFragment> =
  80. createErrorResultGuard(input => !!input.total);
  81. const { server, adminClient, shopClient } = createTestEnvironment(
  82. mergeConfig(testConfig(), {
  83. orderOptions: { process: [customOrderProcess as any, customOrderProcess2 as any] },
  84. paymentOptions: {
  85. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  86. },
  87. }),
  88. );
  89. beforeAll(async () => {
  90. await server.init({
  91. initialData: {
  92. ...initialData,
  93. paymentMethods: [
  94. {
  95. name: testSuccessfulPaymentMethod.code,
  96. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  97. },
  98. ],
  99. },
  100. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  101. customerCount: 1,
  102. });
  103. await adminClient.asSuperAdmin();
  104. }, TEST_SETUP_TIMEOUT_MS);
  105. afterAll(async () => {
  106. await server.destroy();
  107. });
  108. describe('Initial transition', () => {
  109. it('transitions from Created to AddingItems on creation', async () => {
  110. transitionStartSpy.mockClear();
  111. transitionEndSpy.mockClear();
  112. await shopClient.asAnonymousUser();
  113. await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
  114. ADD_ITEM_TO_ORDER,
  115. {
  116. productVariantId: 'T_1',
  117. quantity: 1,
  118. },
  119. );
  120. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  121. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  122. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  123. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  124. });
  125. });
  126. describe('CustomOrderProcess', () => {
  127. it('CustomOrderProcess is injectable', () => {
  128. expect(initSpy).toHaveBeenCalled();
  129. expect(initSpy.mock.calls[0][0]).toBe('default');
  130. });
  131. it('replaced transition target', async () => {
  132. await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
  133. ADD_ITEM_TO_ORDER,
  134. {
  135. productVariantId: 'T_1',
  136. quantity: 1,
  137. },
  138. );
  139. const { nextOrderStates } = await shopClient.query<Codegen.GetNextOrderStatesQuery>(
  140. GET_NEXT_STATES,
  141. );
  142. expect(nextOrderStates).toEqual(['ValidatingCustomer']);
  143. });
  144. it('custom onTransitionStart handler returning false', async () => {
  145. transitionStartSpy.mockClear();
  146. transitionEndSpy.mockClear();
  147. const { transitionOrderToState } = await shopClient.query<
  148. Codegen.TransitionToStateMutation,
  149. Codegen.TransitionToStateMutationVariables
  150. >(TRANSITION_TO_STATE, {
  151. state: 'ValidatingCustomer',
  152. });
  153. orderErrorGuard.assertSuccess(transitionOrderToState);
  154. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  155. expect(transitionEndSpy).not.toHaveBeenCalled();
  156. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([
  157. 'AddingItems',
  158. 'ValidatingCustomer',
  159. ]);
  160. expect(transitionOrderToState?.state).toBe('AddingItems');
  161. });
  162. it('custom onTransitionStart handler returning error message', async () => {
  163. transitionStartSpy.mockClear();
  164. transitionErrorSpy.mockClear();
  165. await shopClient.query<
  166. Codegen.SetCustomerForOrderMutation,
  167. Codegen.SetCustomerForOrderMutationVariables
  168. >(SET_CUSTOMER, {
  169. input: {
  170. firstName: 'Joe',
  171. lastName: 'Test',
  172. emailAddress: 'joetest@gmail.com',
  173. },
  174. });
  175. const { transitionOrderToState } = await shopClient.query<
  176. Codegen.TransitionToStateMutation,
  177. Codegen.TransitionToStateMutationVariables
  178. >(TRANSITION_TO_STATE, {
  179. state: 'ValidatingCustomer',
  180. });
  181. orderErrorGuard.assertErrorResult(transitionOrderToState);
  182. expect(transitionOrderToState!.message).toBe(
  183. 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
  184. );
  185. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  186. expect(transitionOrderToState!.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  187. expect(transitionOrderToState!.fromState).toBe('AddingItems');
  188. expect(transitionOrderToState!.toState).toBe('ValidatingCustomer');
  189. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  190. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  191. expect(transitionEndSpy).not.toHaveBeenCalled();
  192. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  193. 'AddingItems',
  194. 'ValidatingCustomer',
  195. VALIDATION_ERROR_MESSAGE,
  196. ]);
  197. });
  198. it('custom onTransitionStart handler allows transition', async () => {
  199. transitionEndSpy.mockClear();
  200. await shopClient.query<
  201. Codegen.SetCustomerForOrderMutation,
  202. Codegen.SetCustomerForOrderMutationVariables
  203. >(SET_CUSTOMER, {
  204. input: {
  205. firstName: 'Joe',
  206. lastName: 'Test',
  207. emailAddress: 'joetest@company.com',
  208. },
  209. });
  210. const { transitionOrderToState } = await shopClient.query<
  211. Codegen.TransitionToStateMutation,
  212. Codegen.TransitionToStateMutationVariables
  213. >(TRANSITION_TO_STATE, {
  214. state: 'ValidatingCustomer',
  215. });
  216. orderErrorGuard.assertSuccess(transitionOrderToState);
  217. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  218. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
  219. expect(transitionOrderToState?.state).toBe('ValidatingCustomer');
  220. });
  221. it('composes multiple CustomOrderProcesses', async () => {
  222. transitionEndSpy.mockClear();
  223. transitionEndSpy2.mockClear();
  224. const { nextOrderStates } = await shopClient.query<Codegen.GetNextOrderStatesQuery>(
  225. GET_NEXT_STATES,
  226. );
  227. expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
  228. await shopClient.query<
  229. Codegen.TransitionToStateMutation,
  230. Codegen.TransitionToStateMutationVariables
  231. >(TRANSITION_TO_STATE, {
  232. state: 'AddingItems',
  233. });
  234. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']);
  235. expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([
  236. 'ValidatingCustomer',
  237. 'AddingItems',
  238. ]);
  239. });
  240. });
  241. describe('Admin API transition constraints', () => {
  242. let order: NonNullable<TestOrderFragmentFragment>;
  243. beforeAll(async () => {
  244. await shopClient.asAnonymousUser();
  245. await shopClient.query<Codegen.AddItemToOrderMutation, Codegen.AddItemToOrderMutationVariables>(
  246. ADD_ITEM_TO_ORDER,
  247. {
  248. productVariantId: 'T_1',
  249. quantity: 1,
  250. },
  251. );
  252. await shopClient.query<
  253. Codegen.SetCustomerForOrderMutation,
  254. Codegen.SetCustomerForOrderMutationVariables
  255. >(SET_CUSTOMER, {
  256. input: {
  257. firstName: 'Su',
  258. lastName: 'Test',
  259. emailAddress: 'sutest@company.com',
  260. },
  261. });
  262. await shopClient.query<
  263. Codegen.SetShippingAddressMutation,
  264. Codegen.SetShippingAddressMutationVariables
  265. >(SET_SHIPPING_ADDRESS, {
  266. input: {
  267. fullName: 'name',
  268. streetLine1: '12 the street',
  269. city: 'foo',
  270. postalCode: '123456',
  271. countryCode: 'US',
  272. phoneNumber: '4444444',
  273. },
  274. });
  275. await shopClient.query<
  276. Codegen.SetShippingMethodMutation,
  277. Codegen.SetShippingMethodMutationVariables
  278. >(SET_SHIPPING_METHOD, { id: 'T_1' });
  279. await shopClient.query<
  280. Codegen.TransitionToStateMutation,
  281. Codegen.TransitionToStateMutationVariables
  282. >(TRANSITION_TO_STATE, {
  283. state: 'ValidatingCustomer',
  284. });
  285. const { transitionOrderToState } = await shopClient.query<
  286. Codegen.TransitionToStateMutation,
  287. Codegen.TransitionToStateMutationVariables
  288. >(TRANSITION_TO_STATE, {
  289. state: 'ArrangingPayment',
  290. });
  291. orderErrorGuard.assertSuccess(transitionOrderToState);
  292. order = transitionOrderToState!;
  293. });
  294. it('cannot manually transition to PaymentAuthorized', async () => {
  295. expect(order.state).toBe('ArrangingPayment');
  296. const { transitionOrderToState } = await adminClient.query<
  297. Codegen.AdminTransitionMutation,
  298. Codegen.AdminTransitionMutationVariables
  299. >(ADMIN_TRANSITION_TO_STATE, {
  300. id: order.id,
  301. state: 'PaymentAuthorized',
  302. });
  303. orderErrorGuard.assertErrorResult(transitionOrderToState);
  304. expect(transitionOrderToState!.message).toBe(
  305. 'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"',
  306. );
  307. expect(transitionOrderToState!.transitionError).toBe(
  308. 'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
  309. );
  310. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  311. GET_ORDER,
  312. {
  313. id: order.id,
  314. },
  315. );
  316. expect(result.order?.state).toBe('ArrangingPayment');
  317. });
  318. it('cannot manually transition to PaymentSettled', async () => {
  319. const { transitionOrderToState } = await adminClient.query<
  320. Codegen.AdminTransitionMutation,
  321. Codegen.AdminTransitionMutationVariables
  322. >(ADMIN_TRANSITION_TO_STATE, {
  323. id: order.id,
  324. state: 'PaymentSettled',
  325. });
  326. orderErrorGuard.assertErrorResult(transitionOrderToState);
  327. expect(transitionOrderToState!.message).toBe(
  328. 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
  329. );
  330. expect(transitionOrderToState!.transitionError).toContain(
  331. 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
  332. );
  333. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  334. GET_ORDER,
  335. {
  336. id: order.id,
  337. },
  338. );
  339. expect(result.order?.state).toBe('ArrangingPayment');
  340. });
  341. it('cannot manually transition to Cancelled', async () => {
  342. const { addPaymentToOrder } = await shopClient.query<
  343. Codegen.AddPaymentToOrderMutation,
  344. Codegen.AddPaymentToOrderMutationVariables
  345. >(ADD_PAYMENT, {
  346. input: {
  347. method: testSuccessfulPaymentMethod.code,
  348. metadata: {},
  349. },
  350. });
  351. orderErrorGuard.assertSuccess(addPaymentToOrder);
  352. expect(addPaymentToOrder?.state).toBe('PaymentSettled');
  353. const { transitionOrderToState } = await adminClient.query<
  354. Codegen.AdminTransitionMutation,
  355. Codegen.AdminTransitionMutationVariables
  356. >(ADMIN_TRANSITION_TO_STATE, {
  357. id: order.id,
  358. state: 'Cancelled',
  359. });
  360. orderErrorGuard.assertErrorResult(transitionOrderToState);
  361. expect(transitionOrderToState!.message).toBe(
  362. 'Cannot transition Order from "PaymentSettled" to "Cancelled"',
  363. );
  364. expect(transitionOrderToState!.transitionError).toContain(
  365. 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
  366. );
  367. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  368. GET_ORDER,
  369. {
  370. id: order.id,
  371. },
  372. );
  373. expect(result.order?.state).toBe('PaymentSettled');
  374. });
  375. it('cannot manually transition to PartiallyDelivered', async () => {
  376. const { transitionOrderToState } = await adminClient.query<
  377. Codegen.AdminTransitionMutation,
  378. Codegen.AdminTransitionMutationVariables
  379. >(ADMIN_TRANSITION_TO_STATE, {
  380. id: order.id,
  381. state: 'PartiallyDelivered',
  382. });
  383. orderErrorGuard.assertErrorResult(transitionOrderToState);
  384. expect(transitionOrderToState!.message).toBe(
  385. 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
  386. );
  387. expect(transitionOrderToState!.transitionError).toContain(
  388. 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
  389. );
  390. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  391. GET_ORDER,
  392. {
  393. id: order.id,
  394. },
  395. );
  396. expect(result.order?.state).toBe('PaymentSettled');
  397. });
  398. it('cannot manually transition to PartiallyDelivered', async () => {
  399. const { transitionOrderToState } = await adminClient.query<
  400. Codegen.AdminTransitionMutation,
  401. Codegen.AdminTransitionMutationVariables
  402. >(ADMIN_TRANSITION_TO_STATE, {
  403. id: order.id,
  404. state: 'Delivered',
  405. });
  406. orderErrorGuard.assertErrorResult(transitionOrderToState);
  407. expect(transitionOrderToState!.message).toBe(
  408. 'Cannot transition Order from "PaymentSettled" to "Delivered"',
  409. );
  410. expect(transitionOrderToState!.transitionError).toContain(
  411. 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
  412. );
  413. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  414. GET_ORDER,
  415. {
  416. id: order.id,
  417. },
  418. );
  419. expect(result.order?.state).toBe('PaymentSettled');
  420. });
  421. });
  422. });