1
0

order-process.e2e-spec.ts 16 KB

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