mollie-payment.e2e-spec.ts 31 KB

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