order-process.e2e-spec.ts 15 KB

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