order-process.e2e-spec.ts 20 KB

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