payment-process.e2e-spec.ts 14 KB

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