order-process.e2e-spec.ts 17 KB

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