order-process.e2e-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. /* tslint:disable:no-non-null-assertion */
  2. import { CustomOrderProcess, mergeConfig, OrderState } 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.getConnection().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<
  80. TestOrderFragmentFragment | OrderFragment
  81. > = createErrorResultGuard(input => !!input.total);
  82. const { server, adminClient, shopClient } = createTestEnvironment(
  83. mergeConfig(testConfig, {
  84. orderOptions: { process: [customOrderProcess as any, customOrderProcess2 as any] },
  85. paymentOptions: {
  86. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  87. },
  88. }),
  89. );
  90. beforeAll(async () => {
  91. await server.init({
  92. initialData: {
  93. ...initialData,
  94. paymentMethods: [
  95. {
  96. name: testSuccessfulPaymentMethod.code,
  97. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  98. },
  99. ],
  100. },
  101. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  102. customerCount: 1,
  103. });
  104. await adminClient.asSuperAdmin();
  105. }, TEST_SETUP_TIMEOUT_MS);
  106. afterAll(async () => {
  107. await server.destroy();
  108. });
  109. describe('Initial transition', () => {
  110. it('transitions from Created to AddingItems on creation', async () => {
  111. transitionStartSpy.mockClear();
  112. transitionEndSpy.mockClear();
  113. await shopClient.asAnonymousUser();
  114. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  115. productVariantId: 'T_1',
  116. quantity: 1,
  117. });
  118. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  119. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  120. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  121. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  122. });
  123. });
  124. describe('CustomOrderProcess', () => {
  125. it('CustomOrderProcess is injectable', () => {
  126. expect(initSpy).toHaveBeenCalled();
  127. expect(initSpy.mock.calls[0][0]).toBe('default');
  128. });
  129. it('replaced transition target', async () => {
  130. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  131. productVariantId: 'T_1',
  132. quantity: 1,
  133. });
  134. const { nextOrderStates } = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
  135. expect(nextOrderStates).toEqual(['ValidatingCustomer']);
  136. });
  137. it('custom onTransitionStart handler returning false', async () => {
  138. transitionStartSpy.mockClear();
  139. transitionEndSpy.mockClear();
  140. const { transitionOrderToState } = await shopClient.query<
  141. TransitionToState.Mutation,
  142. TransitionToState.Variables
  143. >(TRANSITION_TO_STATE, {
  144. state: 'ValidatingCustomer',
  145. });
  146. orderErrorGuard.assertSuccess(transitionOrderToState);
  147. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  148. expect(transitionEndSpy).not.toHaveBeenCalled();
  149. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([
  150. 'AddingItems',
  151. 'ValidatingCustomer',
  152. ]);
  153. expect(transitionOrderToState?.state).toBe('AddingItems');
  154. });
  155. it('custom onTransitionStart handler returning error message', async () => {
  156. transitionStartSpy.mockClear();
  157. transitionErrorSpy.mockClear();
  158. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  159. SET_CUSTOMER,
  160. {
  161. input: {
  162. firstName: 'Joe',
  163. lastName: 'Test',
  164. emailAddress: 'joetest@gmail.com',
  165. },
  166. },
  167. );
  168. const { transitionOrderToState } = await shopClient.query<
  169. TransitionToState.Mutation,
  170. TransitionToState.Variables
  171. >(TRANSITION_TO_STATE, {
  172. state: 'ValidatingCustomer',
  173. });
  174. orderErrorGuard.assertErrorResult(transitionOrderToState);
  175. expect(transitionOrderToState!.message).toBe(
  176. 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
  177. );
  178. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  179. expect(transitionOrderToState!.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  180. expect(transitionOrderToState!.fromState).toBe('AddingItems');
  181. expect(transitionOrderToState!.toState).toBe('ValidatingCustomer');
  182. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  183. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  184. expect(transitionEndSpy).not.toHaveBeenCalled();
  185. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  186. 'AddingItems',
  187. 'ValidatingCustomer',
  188. VALIDATION_ERROR_MESSAGE,
  189. ]);
  190. });
  191. it('custom onTransitionStart handler allows transition', async () => {
  192. transitionEndSpy.mockClear();
  193. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  194. SET_CUSTOMER,
  195. {
  196. input: {
  197. firstName: 'Joe',
  198. lastName: 'Test',
  199. emailAddress: 'joetest@company.com',
  200. },
  201. },
  202. );
  203. const { transitionOrderToState } = await shopClient.query<
  204. TransitionToState.Mutation,
  205. TransitionToState.Variables
  206. >(TRANSITION_TO_STATE, {
  207. state: 'ValidatingCustomer',
  208. });
  209. orderErrorGuard.assertSuccess(transitionOrderToState);
  210. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  211. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
  212. expect(transitionOrderToState?.state).toBe('ValidatingCustomer');
  213. });
  214. it('composes multiple CustomOrderProcesses', async () => {
  215. transitionEndSpy.mockClear();
  216. transitionEndSpy2.mockClear();
  217. const { nextOrderStates } = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
  218. expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
  219. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
  220. TRANSITION_TO_STATE,
  221. {
  222. state: 'AddingItems',
  223. },
  224. );
  225. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']);
  226. expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([
  227. 'ValidatingCustomer',
  228. 'AddingItems',
  229. ]);
  230. });
  231. });
  232. describe('Admin API transition constraints', () => {
  233. let order: NonNullable<TestOrderFragmentFragment>;
  234. beforeAll(async () => {
  235. await shopClient.asAnonymousUser();
  236. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  237. productVariantId: 'T_1',
  238. quantity: 1,
  239. });
  240. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  241. SET_CUSTOMER,
  242. {
  243. input: {
  244. firstName: 'Su',
  245. lastName: 'Test',
  246. emailAddress: 'sutest@company.com',
  247. },
  248. },
  249. );
  250. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  251. SET_SHIPPING_ADDRESS,
  252. {
  253. input: {
  254. fullName: 'name',
  255. streetLine1: '12 the street',
  256. city: 'foo',
  257. postalCode: '123456',
  258. countryCode: 'US',
  259. phoneNumber: '4444444',
  260. },
  261. },
  262. );
  263. await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
  264. SET_SHIPPING_METHOD,
  265. { id: 'T_1' },
  266. );
  267. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
  268. TRANSITION_TO_STATE,
  269. {
  270. state: 'ValidatingCustomer',
  271. },
  272. );
  273. const { transitionOrderToState } = await shopClient.query<
  274. TransitionToState.Mutation,
  275. TransitionToState.Variables
  276. >(TRANSITION_TO_STATE, {
  277. state: 'ArrangingPayment',
  278. });
  279. orderErrorGuard.assertSuccess(transitionOrderToState);
  280. order = transitionOrderToState!;
  281. });
  282. it('cannot manually transition to PaymentAuthorized', async () => {
  283. expect(order.state).toBe('ArrangingPayment');
  284. const { transitionOrderToState } = await adminClient.query<
  285. AdminTransition.Mutation,
  286. AdminTransition.Variables
  287. >(ADMIN_TRANSITION_TO_STATE, {
  288. id: order.id,
  289. state: 'PaymentAuthorized',
  290. });
  291. orderErrorGuard.assertErrorResult(transitionOrderToState);
  292. expect(transitionOrderToState!.message).toBe(
  293. 'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"',
  294. );
  295. expect(transitionOrderToState!.transitionError).toBe(
  296. 'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
  297. );
  298. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  299. id: order.id,
  300. });
  301. expect(result.order?.state).toBe('ArrangingPayment');
  302. });
  303. it('cannot manually transition to PaymentSettled', async () => {
  304. const { transitionOrderToState } = await adminClient.query<
  305. AdminTransition.Mutation,
  306. AdminTransition.Variables
  307. >(ADMIN_TRANSITION_TO_STATE, {
  308. id: order.id,
  309. state: 'PaymentSettled',
  310. });
  311. orderErrorGuard.assertErrorResult(transitionOrderToState);
  312. expect(transitionOrderToState!.message).toBe(
  313. 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
  314. );
  315. expect(transitionOrderToState!.transitionError).toContain(
  316. 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
  317. );
  318. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  319. id: order.id,
  320. });
  321. expect(result.order?.state).toBe('ArrangingPayment');
  322. });
  323. it('cannot manually transition to Cancelled', async () => {
  324. const { addPaymentToOrder } = await shopClient.query<
  325. AddPaymentToOrder.Mutation,
  326. AddPaymentToOrder.Variables
  327. >(ADD_PAYMENT, {
  328. input: {
  329. method: testSuccessfulPaymentMethod.code,
  330. metadata: {},
  331. },
  332. });
  333. orderErrorGuard.assertSuccess(addPaymentToOrder);
  334. expect(addPaymentToOrder?.state).toBe('PaymentSettled');
  335. const { transitionOrderToState } = await adminClient.query<
  336. AdminTransition.Mutation,
  337. AdminTransition.Variables
  338. >(ADMIN_TRANSITION_TO_STATE, {
  339. id: order.id,
  340. state: 'Cancelled',
  341. });
  342. orderErrorGuard.assertErrorResult(transitionOrderToState);
  343. expect(transitionOrderToState!.message).toBe(
  344. 'Cannot transition Order from "PaymentSettled" to "Cancelled"',
  345. );
  346. expect(transitionOrderToState!.transitionError).toContain(
  347. 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
  348. );
  349. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  350. id: order.id,
  351. });
  352. expect(result.order?.state).toBe('PaymentSettled');
  353. });
  354. it('cannot manually transition to PartiallyDelivered', async () => {
  355. const { transitionOrderToState } = await adminClient.query<
  356. AdminTransition.Mutation,
  357. AdminTransition.Variables
  358. >(ADMIN_TRANSITION_TO_STATE, {
  359. id: order.id,
  360. state: 'PartiallyDelivered',
  361. });
  362. orderErrorGuard.assertErrorResult(transitionOrderToState);
  363. expect(transitionOrderToState!.message).toBe(
  364. 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
  365. );
  366. expect(transitionOrderToState!.transitionError).toContain(
  367. 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
  368. );
  369. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  370. id: order.id,
  371. });
  372. expect(result.order?.state).toBe('PaymentSettled');
  373. });
  374. it('cannot manually transition to PartiallyDelivered', async () => {
  375. const { transitionOrderToState } = await adminClient.query<
  376. AdminTransition.Mutation,
  377. AdminTransition.Variables
  378. >(ADMIN_TRANSITION_TO_STATE, {
  379. id: order.id,
  380. state: 'Delivered',
  381. });
  382. orderErrorGuard.assertErrorResult(transitionOrderToState);
  383. expect(transitionOrderToState!.message).toBe(
  384. 'Cannot transition Order from "PaymentSettled" to "Delivered"',
  385. );
  386. expect(transitionOrderToState!.transitionError).toContain(
  387. 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
  388. );
  389. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  390. id: order.id,
  391. });
  392. expect(result.order?.state).toBe('PaymentSettled');
  393. });
  394. });
  395. });