mollie-payment.e2e-spec.ts 30 KB

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