mollie-payment.e2e-spec.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. import { OrderStatus } from '@mollie/api-client';
  2. import {
  3. ChannelService,
  4. EventBus,
  5. LanguageCode,
  6. Logger,
  7. mergeConfig,
  8. OrderPlacedEvent,
  9. OrderService,
  10. RequestContext,
  11. } from '@vendure/core';
  12. import {
  13. SettlePaymentMutation,
  14. SettlePaymentMutationVariables,
  15. } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
  16. import { SETTLE_PAYMENT } from '@vendure/core/e2e/graphql/shared-definitions';
  17. import {
  18. createTestEnvironment,
  19. E2E_DEFAULT_CHANNEL_TOKEN,
  20. SimpleGraphQLClient,
  21. TestServer,
  22. } from '@vendure/testing';
  23. import nock from 'nock';
  24. import fetch from 'node-fetch';
  25. import path from 'path';
  26. import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
  27. import { initialData } from '../../../e2e-common/e2e-initial-data';
  28. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  29. import { UPDATE_PRODUCT_VARIANTS } from '../../core/e2e/graphql/shared-definitions';
  30. import { MolliePlugin } from '../src/mollie';
  31. import { molliePaymentHandler } from '../src/mollie/mollie.handler';
  32. import { mollieMockData } from './fixtures/mollie-mock-data';
  33. import { CREATE_PAYMENT_METHOD, GET_CUSTOMER_LIST, GET_ORDER_PAYMENTS } from './graphql/admin-queries';
  34. import {
  35. CreatePaymentMethodMutation,
  36. CreatePaymentMethodMutationVariables,
  37. GetCustomerListQuery,
  38. GetCustomerListQueryVariables,
  39. } from './graphql/generated-admin-types';
  40. import {
  41. AddItemToOrderMutation,
  42. AddItemToOrderMutationVariables,
  43. AdjustOrderLineMutation,
  44. AdjustOrderLineMutationVariables,
  45. GetOrderByCodeQuery,
  46. GetOrderByCodeQueryVariables,
  47. TestOrderFragmentFragment,
  48. } from './graphql/generated-shop-types';
  49. import {
  50. ADD_ITEM_TO_ORDER,
  51. ADJUST_ORDER_LINE,
  52. APPLY_COUPON_CODE,
  53. GET_ORDER_BY_CODE,
  54. } from './graphql/shop-queries';
  55. import {
  56. addManualPayment,
  57. CREATE_MOLLIE_PAYMENT_INTENT,
  58. createFixedDiscountCoupon,
  59. createFreeShippingCoupon,
  60. GET_MOLLIE_PAYMENT_METHODS,
  61. refundOrderLine,
  62. setShipping,
  63. testPaymentEligibilityChecker,
  64. } from './payment-helpers';
  65. let shopClient: SimpleGraphQLClient;
  66. let adminClient: SimpleGraphQLClient;
  67. let server: TestServer;
  68. let started = false;
  69. let customers: GetCustomerListQuery['customers']['items'];
  70. let order: TestOrderFragmentFragment;
  71. let serverPort: number;
  72. const SURCHARGE_AMOUNT = -20000;
  73. describe('Mollie payments', () => {
  74. beforeAll(async () => {
  75. const devConfig = mergeConfig(testConfig(), {
  76. plugins: [MolliePlugin.init({ vendureHost: mollieMockData.host })],
  77. paymentOptions: {
  78. paymentMethodEligibilityCheckers: [testPaymentEligibilityChecker],
  79. },
  80. });
  81. const env = createTestEnvironment(devConfig);
  82. serverPort = devConfig.apiOptions.port;
  83. shopClient = env.shopClient;
  84. adminClient = env.adminClient;
  85. server = env.server;
  86. await server.init({
  87. initialData,
  88. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  89. customerCount: 2,
  90. });
  91. started = true;
  92. await adminClient.asSuperAdmin();
  93. ({
  94. customers: { items: customers },
  95. } = await adminClient.query<GetCustomerListQuery, GetCustomerListQueryVariables>(GET_CUSTOMER_LIST, {
  96. options: {
  97. take: 2,
  98. },
  99. }));
  100. }, TEST_SETUP_TIMEOUT_MS);
  101. afterAll(async () => {
  102. await server.destroy();
  103. });
  104. afterEach(() => {
  105. nock.cleanAll();
  106. });
  107. it('Should start successfully', () => {
  108. expect(started).toEqual(true);
  109. expect(customers).toHaveLength(2);
  110. });
  111. it('Should create a Mollie paymentMethod', async () => {
  112. const { createPaymentMethod } = await adminClient.query<
  113. CreatePaymentMethodMutation,
  114. CreatePaymentMethodMutationVariables
  115. >(CREATE_PAYMENT_METHOD, {
  116. input: {
  117. code: mollieMockData.methodCode,
  118. enabled: true,
  119. checker: {
  120. code: testPaymentEligibilityChecker.code,
  121. arguments: [],
  122. },
  123. handler: {
  124. code: molliePaymentHandler.code,
  125. arguments: [
  126. { name: 'redirectUrl', value: mollieMockData.redirectUrl },
  127. { name: 'apiKey', value: mollieMockData.apiKey },
  128. { name: 'autoCapture', value: 'false' },
  129. ],
  130. },
  131. translations: [
  132. {
  133. languageCode: LanguageCode.en,
  134. name: 'Mollie payment test',
  135. description: 'This is a Mollie test payment method',
  136. },
  137. ],
  138. },
  139. });
  140. expect(createPaymentMethod.code).toBe(mollieMockData.methodCode);
  141. });
  142. describe('Payment intent creation', () => {
  143. it('Should prepare an order', async () => {
  144. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  145. const { addItemToOrder } = await shopClient.query<
  146. AddItemToOrderMutation,
  147. AddItemToOrderMutationVariables
  148. >(ADD_ITEM_TO_ORDER, {
  149. productVariantId: 'T_5',
  150. quantity: 10,
  151. });
  152. order = addItemToOrder as TestOrderFragmentFragment;
  153. // Add surcharge
  154. const ctx = new RequestContext({
  155. apiType: 'admin',
  156. isAuthorized: true,
  157. authorizedAsOwnerOnly: false,
  158. channel: await server.app.get(ChannelService).getDefaultChannel(),
  159. });
  160. await server.app.get(OrderService).addSurchargeToOrder(ctx, order.id.replace('T_', ''), {
  161. description: 'Negative test surcharge',
  162. listPrice: SURCHARGE_AMOUNT,
  163. });
  164. expect(order.code).toBeDefined();
  165. });
  166. it('Should fail to create payment intent without shippingmethod', async () => {
  167. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  168. const { createMolliePaymentIntent: result } = await shopClient.query(
  169. CREATE_MOLLIE_PAYMENT_INTENT,
  170. {
  171. input: {
  172. paymentMethodCode: mollieMockData.methodCode,
  173. },
  174. },
  175. );
  176. expect(result.errorCode).toBe('ORDER_PAYMENT_STATE_ERROR');
  177. });
  178. it('Should fail to get payment url when items are out of stock', async () => {
  179. let { updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
  180. input: {
  181. id: 'T_5',
  182. trackInventory: 'TRUE',
  183. outOfStockThreshold: 0,
  184. stockOnHand: 1,
  185. },
  186. });
  187. expect(updateProductVariants[0].stockOnHand).toBe(1);
  188. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  189. await setShipping(shopClient);
  190. const { createMolliePaymentIntent: result } = await shopClient.query(
  191. CREATE_MOLLIE_PAYMENT_INTENT,
  192. {
  193. input: {
  194. paymentMethodCode: mollieMockData.methodCode,
  195. },
  196. },
  197. );
  198. expect(result.message).toContain('insufficient stock of Pinelab stickers');
  199. // Set stock back to not tracking
  200. ({ updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
  201. input: {
  202. id: 'T_5',
  203. trackInventory: 'FALSE',
  204. },
  205. }));
  206. expect(updateProductVariants[0].trackInventory).toBe('FALSE');
  207. });
  208. it('Should get payment url without Mollie method', async () => {
  209. let mollieRequest: any | undefined;
  210. nock('https://api.mollie.com/')
  211. .post('/v2/payments', body => {
  212. mollieRequest = body;
  213. return true;
  214. })
  215. .reply(200, mollieMockData.molliePaymentResponse);
  216. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  217. input: {
  218. paymentMethodCode: mollieMockData.methodCode,
  219. redirectUrl: 'given-storefront-redirect-url',
  220. locale: 'nl_NL',
  221. },
  222. });
  223. expect(createMolliePaymentIntent).toEqual({
  224. url: 'https://www.mollie.com/checkout/select-method/mock-payment',
  225. });
  226. expect(mollieRequest?.description).toEqual(order.code);
  227. expect(mollieRequest?.redirectUrl).toEqual('given-storefront-redirect-url');
  228. expect(mollieRequest?.locale).toEqual('nl_NL');
  229. expect(mollieRequest?.captureMode).toEqual('automatic'); // Default should be automatic
  230. expect(mollieRequest?.webhookUrl).toEqual(
  231. `${mollieMockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
  232. );
  233. expect(mollieRequest?.amount?.value).toBe('1009.88');
  234. expect(mollieRequest?.amount?.currency).toBe('USD');
  235. expect(mollieRequest.lines[0].vatAmount.value).toEqual('199.98');
  236. const totalLineAmount =
  237. mollieRequest?.lines?.reduce(
  238. (sum: number, line: any) => sum + Number(line.totalAmount.value),
  239. 0,
  240. ) ?? 0;
  241. // Sum of lines should equal order total
  242. expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
  243. });
  244. it('Should use fallback redirect appended with order code, when no redirect is given', async () => {
  245. let mollieRequest: any | undefined;
  246. nock('https://api.mollie.com/')
  247. .post('/v2/payments', body => {
  248. mollieRequest = body;
  249. return true;
  250. })
  251. .reply(200, mollieMockData.molliePaymentResponse);
  252. await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  253. input: {
  254. paymentMethodCode: mollieMockData.methodCode,
  255. },
  256. });
  257. expect(mollieRequest?.redirectUrl).toEqual(`${mollieMockData.redirectUrl}/${order.code}`);
  258. });
  259. it('Should get payment url with Mollie method', async () => {
  260. nock('https://api.mollie.com/')
  261. .post('/v2/payments')
  262. .reply(200, mollieMockData.molliePaymentResponse);
  263. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  264. await setShipping(shopClient);
  265. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  266. input: {
  267. paymentMethodCode: mollieMockData.methodCode,
  268. molliePaymentMethodCode: 'ideal',
  269. },
  270. });
  271. expect(createMolliePaymentIntent).toEqual({
  272. url: 'https://www.mollie.com/checkout/select-method/mock-payment',
  273. });
  274. });
  275. it('Should not allow creating intent if payment method is not eligible', async () => {
  276. // Set quantity to 9, which is not allowe by our test eligibility checker
  277. await shopClient.query<AdjustOrderLineMutation, AdjustOrderLineMutationVariables>(
  278. ADJUST_ORDER_LINE,
  279. {
  280. orderLineId: order.lines[0].id,
  281. quantity: 9,
  282. },
  283. );
  284. let mollieRequest: any | undefined;
  285. nock('https://api.mollie.com/')
  286. .post('/v2/payments', body => {
  287. mollieRequest = body;
  288. return true;
  289. })
  290. .reply(200, mollieMockData.molliePaymentResponse);
  291. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  292. input: {
  293. paymentMethodCode: mollieMockData.methodCode,
  294. redirectUrl: 'given-storefront-redirect-url',
  295. },
  296. });
  297. expect(createMolliePaymentIntent.errorCode).toBe('INELIGIBLE_PAYMENT_METHOD_ERROR');
  298. expect(createMolliePaymentIntent.message).toContain('is not eligible for order');
  299. });
  300. it('Should get payment url with deducted amount if a payment is already made', async () => {
  301. // Change quantity back to 10
  302. await shopClient.query<AdjustOrderLineMutation, AdjustOrderLineMutationVariables>(
  303. ADJUST_ORDER_LINE,
  304. {
  305. orderLineId: order.lines[0].id,
  306. quantity: 10,
  307. },
  308. );
  309. let mollieRequest: any | undefined;
  310. nock('https://api.mollie.com/')
  311. .post('/v2/payments', body => {
  312. mollieRequest = body;
  313. return true;
  314. })
  315. .reply(200, mollieMockData.molliePaymentResponse);
  316. await addManualPayment(server, 1, 10000);
  317. await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  318. input: {
  319. paymentMethodCode: mollieMockData.methodCode,
  320. },
  321. });
  322. expect(mollieRequest?.amount?.value).toBe('909.88'); // minus 100,00 from manual payment
  323. const totalLineAmount =
  324. mollieRequest?.lines?.reduce(
  325. (sum: number, line: any) => sum + Number(line.totalAmount.value),
  326. 0,
  327. ) ?? 0;
  328. // This is the line that deducts the amount from payment that was already made
  329. const priceDeductionLine = mollieRequest.lines?.find((line: any) => line.totalAmount.value < 0);
  330. expect(priceDeductionLine?.type).toBe('store_credit');
  331. // Sum of lines should equal order total
  332. expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
  333. });
  334. it('Should create intent as admin', async () => {
  335. nock('https://api.mollie.com/')
  336. .post('/v2/payments')
  337. .reply(200, mollieMockData.molliePaymentResponse);
  338. // Admin API passes order ID, and no payment method code
  339. const { createMolliePaymentIntent: intent } = await adminClient.query(
  340. CREATE_MOLLIE_PAYMENT_INTENT,
  341. {
  342. input: {
  343. orderId: '1',
  344. },
  345. },
  346. );
  347. expect(intent.url).toBe(mollieMockData.molliePaymentResponse._links.checkout.href);
  348. });
  349. it('Should get available paymentMethods', async () => {
  350. nock('https://api.mollie.com/')
  351. .get('/v2/methods?resource=orders')
  352. .reply(200, mollieMockData.molliePaymentMethodsResponse);
  353. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  354. const { molliePaymentMethods } = await shopClient.query(GET_MOLLIE_PAYMENT_METHODS, {
  355. input: {
  356. paymentMethodCode: mollieMockData.methodCode,
  357. },
  358. });
  359. const method = molliePaymentMethods[0];
  360. expect(method.code).toEqual('ideal');
  361. expect(method.minimumAmount).toBeDefined();
  362. expect(method.maximumAmount).toBeDefined();
  363. expect(method.image).toBeDefined();
  364. });
  365. it('Transitions to PaymentSettled for orders with a total of $0', async () => {
  366. await shopClient.asUserWithCredentials(customers[1].emailAddress, 'test');
  367. const { addItemToOrder } = await shopClient.query(ADD_ITEM_TO_ORDER, {
  368. productVariantId: 'T_1',
  369. quantity: 1,
  370. });
  371. await setShipping(shopClient);
  372. // Discount the order so it has a total of $0
  373. await createFixedDiscountCoupon(adminClient, 156880, 'DISCOUNT_ORDER');
  374. await createFreeShippingCoupon(adminClient, 'FREE_SHIPPING');
  375. await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'DISCOUNT_ORDER' });
  376. await shopClient.query(APPLY_COUPON_CODE, { couponCode: 'FREE_SHIPPING' });
  377. // Create payment intent
  378. const { createMolliePaymentIntent: intent } = await shopClient.query(
  379. CREATE_MOLLIE_PAYMENT_INTENT,
  380. {
  381. input: {
  382. paymentMethodCode: mollieMockData.methodCode,
  383. redirectUrl: 'https://my-storefront.io/order-confirmation',
  384. },
  385. },
  386. );
  387. const { orderByCode } = await shopClient.query(GET_ORDER_BY_CODE, { code: addItemToOrder.code });
  388. expect(intent.url).toBe('https://my-storefront.io/order-confirmation');
  389. expect(orderByCode.totalWithTax).toBe(0);
  390. expect(orderByCode.state).toBe('PaymentSettled');
  391. });
  392. });
  393. describe('Handle standard payment methods', () => {
  394. it('Should transition to ArrangingPayment when partially paid', async () => {
  395. nock('https://api.mollie.com/')
  396. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  397. .reply(200, {
  398. ...mollieMockData.molliePaymentResponse,
  399. // Mollie says: Only 20.00 was paid
  400. amount: { value: '20.00', currency: 'EUR' },
  401. description: order.code,
  402. status: OrderStatus.paid,
  403. });
  404. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  405. method: 'post',
  406. body: JSON.stringify({ id: mollieMockData.molliePaymentResponse.id }),
  407. headers: { 'Content-Type': 'application/json' },
  408. });
  409. const { order: adminOrder } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order?.id });
  410. expect(adminOrder.state).toBe('ArrangingPayment');
  411. });
  412. let orderPlacedEvent: OrderPlacedEvent | undefined;
  413. it('Should place order after paying outstanding amount', async () => {
  414. server.app
  415. .get(EventBus)
  416. .ofType(OrderPlacedEvent)
  417. .subscribe(event => {
  418. orderPlacedEvent = event;
  419. });
  420. nock('https://api.mollie.com/')
  421. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  422. .reply(200, {
  423. ...mollieMockData.molliePaymentResponse,
  424. // Add a payment of remaining 1089.90
  425. amount: { value: '1089.90', currency: 'EUR' }, // 1109.90 minus the previously paid 20.00 = 1089.90
  426. description: order.code,
  427. status: OrderStatus.paid,
  428. });
  429. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  430. method: 'post',
  431. body: JSON.stringify({ id: mollieMockData.molliePaymentResponse.id }),
  432. headers: { 'Content-Type': 'application/json' },
  433. });
  434. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  435. const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
  436. GET_ORDER_BY_CODE,
  437. {
  438. code: order.code,
  439. },
  440. );
  441. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  442. order = orderByCode!;
  443. expect(order.state).toBe('PaymentSettled');
  444. });
  445. it('Should log error when order is paid again with a another payment', async () => {
  446. const errorLogSpy = vi.spyOn(Logger, 'error');
  447. nock('https://api.mollie.com/')
  448. .get(`/v2/payments/tr_newMockId`)
  449. .reply(200, {
  450. ...mollieMockData.molliePaymentResponse,
  451. id: 'tr_newMockId',
  452. amount: { value: '100', currency: 'EUR' }, // Try to pay another 100
  453. description: order.code,
  454. status: OrderStatus.paid,
  455. });
  456. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  457. method: 'post',
  458. body: JSON.stringify({ id: 'tr_newMockId' }),
  459. headers: { 'Content-Type': 'application/json' },
  460. });
  461. const logMessage = errorLogSpy.mock.calls?.[0]?.[0];
  462. expect(logMessage).toBe(
  463. `Order '${order.code}' is already paid. Mollie payment 'tr_newMockId' should be refunded.`,
  464. );
  465. });
  466. it('Should have preserved original languageCode ', () => {
  467. // We've set the languageCode to 'nl' in the mock response's metadata
  468. expect(orderPlacedEvent?.ctx.languageCode).toBe('nl');
  469. });
  470. it('Resulting events should have a ctx.req ', () => {
  471. // We've set the languageCode to 'nl' in the mock response's metadata
  472. expect(orderPlacedEvent?.ctx?.req).toBeDefined();
  473. });
  474. it('Should have Mollie metadata on payment', async () => {
  475. const {
  476. order: { payments },
  477. } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order.id });
  478. const metadata = payments[1].metadata;
  479. expect(metadata.mode).toBe(mollieMockData.molliePaymentResponse.mode);
  480. expect(metadata.profileId).toBe(mollieMockData.molliePaymentResponse.profileId);
  481. expect(metadata.authorizedAt).toEqual(mollieMockData.molliePaymentResponse.authorizedAt);
  482. expect(metadata.paidAt).toEqual(mollieMockData.molliePaymentResponse.paidAt);
  483. });
  484. it('Should fail to refund', async () => {
  485. nock('https://api.mollie.com/')
  486. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  487. .reply(200, mollieMockData.molliePaymentResponse);
  488. nock('https://api.mollie.com/')
  489. .post('/v2/payments/tr_mockPayment/refunds')
  490. .reply(200, { status: 'failed', resource: 'payment' });
  491. const refund = await refundOrderLine(
  492. adminClient,
  493. order.lines[0].id,
  494. 1,
  495. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  496. order!.payments![1].id,
  497. SURCHARGE_AMOUNT,
  498. );
  499. expect(refund.state).toBe('Failed');
  500. });
  501. it('Should successfully refund the Mollie payment', async () => {
  502. let mollieRequest: any;
  503. nock('https://api.mollie.com/')
  504. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  505. .reply(200, mollieMockData.molliePaymentResponse);
  506. nock('https://api.mollie.com/')
  507. .post('/v2/payments/tr_mockPayment/refunds', body => {
  508. mollieRequest = body;
  509. return true;
  510. })
  511. .reply(200, { status: 'pending', resource: 'payment' });
  512. const refund = await refundOrderLine(
  513. adminClient,
  514. order.lines[0].id,
  515. 10,
  516. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  517. order.payments!.find(p => p.amount === 108990)!.id,
  518. SURCHARGE_AMOUNT,
  519. );
  520. expect(mollieRequest?.amount.value).toBe('999.90'); // Only refund mollie amount, not the gift card
  521. expect(refund.total).toBe(99990);
  522. expect(refund.state).toBe('Settled');
  523. });
  524. });
  525. describe('Handle pay-later with manual capture', () => {
  526. it('Should prepare a new order', async () => {
  527. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  528. const { addItemToOrder } = await shopClient.query<
  529. AddItemToOrderMutation,
  530. AddItemToOrderMutationVariables
  531. >(ADD_ITEM_TO_ORDER, {
  532. productVariantId: 'T_1',
  533. quantity: 2,
  534. });
  535. order = addItemToOrder as TestOrderFragmentFragment;
  536. await setShipping(shopClient);
  537. expect(order.code).toBeDefined();
  538. });
  539. it('Should create payment intent with immediateCapture = false', async () => {
  540. let mollieRequest: any;
  541. nock('https://api.mollie.com/')
  542. .post('/v2/payments', body => {
  543. mollieRequest = body;
  544. return true;
  545. })
  546. .reply(200, mollieMockData.molliePaymentResponse);
  547. await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  548. input: {
  549. immediateCapture: false,
  550. },
  551. });
  552. expect(mollieRequest.captureMode).toBe('manual');
  553. });
  554. it('Should authorize payment with immediateCapture = false', async () => {
  555. nock('https://api.mollie.com/')
  556. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  557. .reply(200, {
  558. ...mollieMockData.molliePaymentResponse,
  559. amount: { value: '3127.60', currency: 'EUR' },
  560. description: order.code,
  561. status: OrderStatus.authorized,
  562. });
  563. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  564. method: 'post',
  565. body: JSON.stringify({ id: mollieMockData.molliePaymentResponse.id }),
  566. headers: { 'Content-Type': 'application/json' },
  567. });
  568. const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
  569. GET_ORDER_BY_CODE,
  570. {
  571. code: order.code,
  572. },
  573. );
  574. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  575. order = orderByCode!;
  576. expect(order.state).toBe('PaymentAuthorized');
  577. });
  578. it('Should settle payment via settlePayment mutation', async () => {
  579. // Mock the getOrder Mollie call
  580. nock('https://api.mollie.com/')
  581. .get(`/v2/payments/${mollieMockData.molliePaymentResponse.id}`)
  582. .reply(200, {
  583. ...mollieMockData.molliePaymentResponse,
  584. description: order.code,
  585. status: OrderStatus.authorized,
  586. amount: { value: '3127.60', currency: 'EUR' },
  587. });
  588. // Mock the createCapture call
  589. let createCaptureRequest: any;
  590. nock('https://api.mollie.com/')
  591. .post(`/v2/payments/tr_mockPayment/captures`, body => {
  592. createCaptureRequest = body;
  593. return true;
  594. })
  595. .reply(200, { status: 'pending', id: 'cpt_mockCapture', resource: 'capture' });
  596. // Mock the getCapture call
  597. nock('https://api.mollie.com/')
  598. .get(`/v2/payments/tr_mockPayment/captures/cpt_mockCapture`)
  599. .reply(200, { status: 'succeeded', id: 'cpt_mockCapture', resource: 'capture' });
  600. await adminClient.query<SettlePaymentMutation, SettlePaymentMutationVariables>(SETTLE_PAYMENT, {
  601. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  602. id: order.payments![0].id,
  603. });
  604. const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
  605. GET_ORDER_BY_CODE,
  606. {
  607. code: order.code,
  608. },
  609. );
  610. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  611. order = orderByCode!;
  612. expect(createCaptureRequest.amount.value).toBe('3127.60'); // Full amount
  613. expect(order.state).toBe('PaymentSettled');
  614. });
  615. });
  616. });