payment-process.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /* tslint:disable:no-non-null-assertion */
  2. import {
  3. CustomOrderProcess,
  4. CustomPaymentProcess,
  5. DefaultLogger,
  6. defaultOrderProcess,
  7. LanguageCode,
  8. mergeConfig,
  9. Order,
  10. OrderPlacedStrategy,
  11. OrderState,
  12. PaymentMethodHandler,
  13. RequestContext,
  14. TransactionalConnection,
  15. } from '@vendure/core';
  16. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  17. import gql from 'graphql-tag';
  18. import path from 'path';
  19. import { initialData } from '../../../e2e-common/e2e-initial-data';
  20. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  21. import { ORDER_WITH_LINES_FRAGMENT } from './graphql/fragments';
  22. import * as Codegen from './graphql/generated-e2e-admin-types';
  23. import { ErrorCode } from './graphql/generated-e2e-admin-types';
  24. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  25. import {
  26. ADMIN_TRANSITION_TO_STATE,
  27. GET_ORDER,
  28. TRANSITION_PAYMENT_TO_STATE,
  29. } from './graphql/shared-definitions';
  30. import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, GET_ACTIVE_ORDER } from './graphql/shop-definitions';
  31. import { proceedToArrangingPayment } from './utils/test-order-utils';
  32. const initSpy = jest.fn();
  33. const transitionStartSpy = jest.fn();
  34. const transitionEndSpy = jest.fn();
  35. const transitionErrorSpy = jest.fn();
  36. const settlePaymentSpy = jest.fn();
  37. describe('Payment process', () => {
  38. let orderId: string;
  39. let payment1Id: string;
  40. const PAYMENT_ERROR_MESSAGE = 'Payment is not valid';
  41. const customPaymentProcess: CustomPaymentProcess<'Validating'> = {
  42. init(injector) {
  43. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  44. },
  45. transitions: {
  46. Created: {
  47. to: ['Validating'],
  48. mergeStrategy: 'merge',
  49. },
  50. Validating: {
  51. to: ['Settled', 'Declined', 'Cancelled'],
  52. },
  53. },
  54. onTransitionStart(fromState, toState, data) {
  55. transitionStartSpy(fromState, toState, data);
  56. if (fromState === 'Validating' && toState === 'Settled') {
  57. if (!data.payment.metadata.valid) {
  58. return PAYMENT_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 customOrderProcess: CustomOrderProcess<'ValidatingPayment'> = {
  70. transitions: {
  71. ArrangingPayment: {
  72. to: ['ValidatingPayment'],
  73. mergeStrategy: 'replace',
  74. },
  75. ValidatingPayment: {
  76. to: ['PaymentAuthorized', 'PaymentSettled', 'ArrangingAdditionalPayment'],
  77. },
  78. },
  79. };
  80. const testPaymentHandler = new PaymentMethodHandler({
  81. code: 'test-handler',
  82. description: [{ languageCode: LanguageCode.en, value: 'Test handler' }],
  83. args: {},
  84. createPayment: (ctx, order, amount, args, metadata) => {
  85. return {
  86. state: 'Validating' as any,
  87. amount,
  88. metadata,
  89. };
  90. },
  91. settlePayment: (ctx, order, payment) => {
  92. settlePaymentSpy();
  93. return {
  94. success: true,
  95. };
  96. },
  97. });
  98. class TestOrderPlacedStrategy implements OrderPlacedStrategy {
  99. shouldSetAsPlaced(
  100. ctx: RequestContext,
  101. fromState: OrderState,
  102. toState: OrderState,
  103. order: Order,
  104. ): boolean | Promise<boolean> {
  105. return fromState === 'ArrangingPayment' && toState === ('ValidatingPayment' as any);
  106. }
  107. }
  108. const orderGuard: ErrorResultGuard<CodegenShop.TestOrderFragmentFragment | Codegen.OrderFragment> =
  109. createErrorResultGuard(input => !!input.total);
  110. const paymentGuard: ErrorResultGuard<Codegen.PaymentFragment> = createErrorResultGuard(
  111. input => !!input.id,
  112. );
  113. const { server, adminClient, shopClient } = createTestEnvironment(
  114. mergeConfig(testConfig(), {
  115. // logger: new DefaultLogger(),
  116. orderOptions: {
  117. process: [defaultOrderProcess, customOrderProcess] as any,
  118. orderPlacedStrategy: new TestOrderPlacedStrategy(),
  119. },
  120. paymentOptions: {
  121. paymentMethodHandlers: [testPaymentHandler],
  122. customPaymentProcess: [customPaymentProcess as any],
  123. },
  124. }),
  125. );
  126. beforeAll(async () => {
  127. await server.init({
  128. initialData: {
  129. ...initialData,
  130. paymentMethods: [
  131. {
  132. name: testPaymentHandler.code,
  133. handler: { code: testPaymentHandler.code, arguments: [] },
  134. },
  135. ],
  136. },
  137. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  138. customerCount: 1,
  139. });
  140. await adminClient.asSuperAdmin();
  141. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  142. await shopClient.query<
  143. CodegenShop.AddItemToOrderMutation,
  144. CodegenShop.AddItemToOrderMutationVariables
  145. >(ADD_ITEM_TO_ORDER, {
  146. productVariantId: 'T_1',
  147. quantity: 1,
  148. });
  149. orderId = (await proceedToArrangingPayment(shopClient)) as string;
  150. }, TEST_SETUP_TIMEOUT_MS);
  151. afterAll(async () => {
  152. await server.destroy();
  153. });
  154. it('CustomPaymentProcess is injectable', () => {
  155. expect(initSpy).toHaveBeenCalled();
  156. expect(initSpy.mock.calls[0][0]).toBe('default');
  157. });
  158. it('creates Payment in custom state', async () => {
  159. const { addPaymentToOrder } = await shopClient.query<
  160. CodegenShop.AddPaymentToOrderMutation,
  161. CodegenShop.AddPaymentToOrderMutationVariables
  162. >(ADD_PAYMENT, {
  163. input: {
  164. method: testPaymentHandler.code,
  165. metadata: {
  166. valid: true,
  167. },
  168. },
  169. });
  170. orderGuard.assertSuccess(addPaymentToOrder);
  171. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  172. GET_ORDER,
  173. {
  174. id: orderId,
  175. },
  176. );
  177. expect(order?.state).toBe('ArrangingPayment');
  178. expect(order?.payments?.length).toBe(1);
  179. expect(order?.payments?.[0].state).toBe('Validating');
  180. payment1Id = addPaymentToOrder?.payments?.[0].id!;
  181. });
  182. it('calls transition hooks', async () => {
  183. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'Validating']);
  184. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'Validating']);
  185. expect(transitionErrorSpy).not.toHaveBeenCalled();
  186. });
  187. it('Payment next states', async () => {
  188. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  189. GET_ORDER,
  190. {
  191. id: orderId,
  192. },
  193. );
  194. expect(order?.payments?.[0].nextStates).toEqual(['Settled', 'Declined', 'Cancelled']);
  195. });
  196. it('transition Order to custom state, custom OrderPlacedStrategy sets as placed', async () => {
  197. const { activeOrder: activeOrderPre } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  198. GET_ACTIVE_ORDER,
  199. );
  200. expect(activeOrderPre).not.toBeNull();
  201. const { transitionOrderToState } = await adminClient.query<
  202. Codegen.AdminTransitionMutation,
  203. Codegen.AdminTransitionMutationVariables
  204. >(ADMIN_TRANSITION_TO_STATE, {
  205. id: orderId,
  206. state: 'ValidatingPayment',
  207. });
  208. orderGuard.assertSuccess(transitionOrderToState);
  209. expect(transitionOrderToState.state).toBe('ValidatingPayment');
  210. expect(transitionOrderToState?.active).toBe(false);
  211. const { activeOrder: activeOrderPost } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  212. GET_ACTIVE_ORDER,
  213. );
  214. expect(activeOrderPost).toBeNull();
  215. });
  216. it('transitionPaymentToState succeeds', async () => {
  217. const { transitionPaymentToState } = await adminClient.query<
  218. Codegen.TransitionPaymentToStateMutation,
  219. Codegen.TransitionPaymentToStateMutationVariables
  220. >(TRANSITION_PAYMENT_TO_STATE, {
  221. id: payment1Id,
  222. state: 'Settled',
  223. });
  224. paymentGuard.assertSuccess(transitionPaymentToState);
  225. expect(transitionPaymentToState.state).toBe('Settled');
  226. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  227. GET_ORDER,
  228. {
  229. id: orderId,
  230. },
  231. );
  232. expect(order?.state).toBe('PaymentSettled');
  233. expect(settlePaymentSpy).toHaveBeenCalled();
  234. });
  235. describe('failing, cancelling, and manually adding a Payment', () => {
  236. let order2Id: string;
  237. let payment2Id: string;
  238. beforeAll(async () => {
  239. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  240. await shopClient.query<
  241. CodegenShop.AddItemToOrderMutation,
  242. CodegenShop.AddItemToOrderMutationVariables
  243. >(ADD_ITEM_TO_ORDER, {
  244. productVariantId: 'T_1',
  245. quantity: 1,
  246. });
  247. order2Id = (await proceedToArrangingPayment(shopClient)) as string;
  248. const { addPaymentToOrder } = await shopClient.query<
  249. CodegenShop.AddPaymentToOrderMutation,
  250. CodegenShop.AddPaymentToOrderMutationVariables
  251. >(ADD_PAYMENT, {
  252. input: {
  253. method: testPaymentHandler.code,
  254. metadata: {
  255. valid: false,
  256. },
  257. },
  258. });
  259. orderGuard.assertSuccess(addPaymentToOrder);
  260. payment2Id = addPaymentToOrder!.payments![0].id;
  261. await adminClient.query<
  262. Codegen.AdminTransitionMutation,
  263. Codegen.AdminTransitionMutationVariables
  264. >(ADMIN_TRANSITION_TO_STATE, {
  265. id: order2Id,
  266. state: 'ValidatingPayment',
  267. });
  268. });
  269. it('attempting to transition payment to settled fails', async () => {
  270. const { transitionPaymentToState } = await adminClient.query<
  271. Codegen.TransitionPaymentToStateMutation,
  272. Codegen.TransitionPaymentToStateMutationVariables
  273. >(TRANSITION_PAYMENT_TO_STATE, {
  274. id: payment2Id,
  275. state: 'Settled',
  276. });
  277. paymentGuard.assertErrorResult(transitionPaymentToState);
  278. expect(transitionPaymentToState.errorCode).toBe(ErrorCode.PAYMENT_STATE_TRANSITION_ERROR);
  279. expect((transitionPaymentToState as any).transitionError).toBe(PAYMENT_ERROR_MESSAGE);
  280. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  281. GET_ORDER,
  282. {
  283. id: order2Id,
  284. },
  285. );
  286. expect(order?.state).toBe('ValidatingPayment');
  287. });
  288. it('cancel failed payment', async () => {
  289. const { transitionPaymentToState } = await adminClient.query<
  290. Codegen.TransitionPaymentToStateMutation,
  291. Codegen.TransitionPaymentToStateMutationVariables
  292. >(TRANSITION_PAYMENT_TO_STATE, {
  293. id: payment2Id,
  294. state: 'Cancelled',
  295. });
  296. paymentGuard.assertSuccess(transitionPaymentToState);
  297. expect(transitionPaymentToState.state).toBe('Cancelled');
  298. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  299. GET_ORDER,
  300. {
  301. id: order2Id,
  302. },
  303. );
  304. expect(order?.state).toBe('ValidatingPayment');
  305. });
  306. it('manually adds payment', async () => {
  307. const { transitionOrderToState } = await adminClient.query<
  308. Codegen.AdminTransitionMutation,
  309. Codegen.AdminTransitionMutationVariables
  310. >(ADMIN_TRANSITION_TO_STATE, {
  311. id: order2Id,
  312. state: 'ArrangingAdditionalPayment',
  313. });
  314. orderGuard.assertSuccess(transitionOrderToState);
  315. const { addManualPaymentToOrder } = await adminClient.query<
  316. Codegen.AddManualPayment2Mutation,
  317. Codegen.AddManualPayment2MutationVariables
  318. >(ADD_MANUAL_PAYMENT, {
  319. input: {
  320. orderId: order2Id,
  321. metadata: {},
  322. method: 'manual payment',
  323. transactionId: '12345',
  324. },
  325. });
  326. orderGuard.assertSuccess(addManualPaymentToOrder);
  327. expect(addManualPaymentToOrder.state).toBe('ArrangingAdditionalPayment');
  328. expect(addManualPaymentToOrder.payments![1].state).toBe('Settled');
  329. expect(addManualPaymentToOrder.payments![1].amount).toBe(addManualPaymentToOrder.totalWithTax);
  330. });
  331. it('transitions Order to PaymentSettled', async () => {
  332. const { transitionOrderToState } = await adminClient.query<
  333. Codegen.AdminTransitionMutation,
  334. Codegen.AdminTransitionMutationVariables
  335. >(ADMIN_TRANSITION_TO_STATE, {
  336. id: order2Id,
  337. state: 'PaymentSettled',
  338. });
  339. orderGuard.assertSuccess(transitionOrderToState);
  340. expect(transitionOrderToState.state).toBe('PaymentSettled');
  341. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  342. GET_ORDER,
  343. {
  344. id: order2Id,
  345. },
  346. );
  347. const settledPaymentAmount = order?.payments
  348. ?.filter(p => p.state === 'Settled')
  349. .reduce((sum, p) => sum + p.amount, 0);
  350. expect(settledPaymentAmount).toBe(order?.totalWithTax);
  351. });
  352. });
  353. });
  354. export const ADD_MANUAL_PAYMENT = gql`
  355. mutation AddManualPayment2($input: ManualPaymentInput!) {
  356. addManualPaymentToOrder(input: $input) {
  357. ...OrderWithLines
  358. ... on ErrorResult {
  359. errorCode
  360. message
  361. }
  362. }
  363. }
  364. ${ORDER_WITH_LINES_FRAGMENT}
  365. `;