payment-process.e2e-spec.ts 15 KB

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