payment-process.e2e-spec.ts 15 KB

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