payment-method.e2e-spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. import { CurrencyCode, DeletionResult, ErrorCode } from '@vendure/common/lib/generated-shop-types';
  2. import { dummyPaymentHandler, LanguageCode, PaymentMethodEligibilityChecker } from '@vendure/core';
  3. import {
  4. createErrorResultGuard,
  5. createTestEnvironment,
  6. E2E_DEFAULT_CHANNEL_TOKEN,
  7. ErrorResultGuard,
  8. } from '@vendure/testing';
  9. import path from 'path';
  10. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  11. import { initialData } from '../../../e2e-common/e2e-initial-data';
  12. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  13. import { FragmentOf, graphql } from './graphql/graphql-admin';
  14. import { FragmentOf as FragmentOfShop, ResultOf as ResultOfShop } from './graphql/graphql-shop';
  15. import { createChannelDocument } from './graphql/shared-definitions';
  16. import {
  17. activePaymentMethodsQueryDocument,
  18. addItemToOrderDocument,
  19. addPaymentDocument,
  20. getEligiblePaymentMethodsDocument,
  21. testOrderWithPaymentsFragment,
  22. } from './graphql/shop-definitions';
  23. import { proceedToArrangingPayment } from './utils/test-order-utils';
  24. const checkerSpy = vi.fn();
  25. const minPriceChecker = new PaymentMethodEligibilityChecker({
  26. code: 'min-price-checker',
  27. description: [{ languageCode: LanguageCode.en, value: 'Min price checker' }],
  28. args: {
  29. minPrice: {
  30. type: 'int',
  31. },
  32. },
  33. check(ctx, order, args) {
  34. checkerSpy();
  35. if (order.totalWithTax >= args.minPrice) {
  36. return true;
  37. } else {
  38. return 'Order total too low';
  39. }
  40. },
  41. });
  42. describe('PaymentMethod resolver', () => {
  43. const orderGuard: ErrorResultGuard<FragmentOfShop<typeof testOrderWithPaymentsFragment>> =
  44. createErrorResultGuard(input => !!input.lines);
  45. type PaymentMethodType = FragmentOf<typeof paymentMethodFragment>;
  46. const paymentMethodGuard: ErrorResultGuard<PaymentMethodType> = createErrorResultGuard(
  47. input => !!input.id,
  48. );
  49. type ActivePaymentMethodType = NonNullable<
  50. ResultOfShop<typeof activePaymentMethodsQueryDocument>['activePaymentMethods'][number]
  51. >;
  52. const activePaymentMethodGuard: ErrorResultGuard<ActivePaymentMethodType> = createErrorResultGuard(
  53. input => !!input && !!input.id,
  54. );
  55. const { server, adminClient, shopClient } = createTestEnvironment({
  56. ...testConfig(),
  57. // logger: new DefaultLogger(),
  58. paymentOptions: {
  59. paymentMethodEligibilityCheckers: [minPriceChecker],
  60. paymentMethodHandlers: [dummyPaymentHandler],
  61. },
  62. });
  63. beforeAll(async () => {
  64. await server.init({
  65. initialData,
  66. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  67. customerCount: 2,
  68. });
  69. await adminClient.asSuperAdmin();
  70. }, TEST_SETUP_TIMEOUT_MS);
  71. afterAll(async () => {
  72. await server.destroy();
  73. });
  74. it('create', async () => {
  75. const { createPaymentMethod } = await adminClient.query(createPaymentMethodDocument, {
  76. input: {
  77. code: 'no-checks',
  78. enabled: true,
  79. handler: {
  80. code: dummyPaymentHandler.code,
  81. arguments: [{ name: 'automaticSettle', value: 'true' }],
  82. },
  83. translations: [
  84. {
  85. languageCode: LanguageCode.en,
  86. name: 'No Checker',
  87. description: 'This is a test payment method',
  88. },
  89. ],
  90. },
  91. });
  92. expect(createPaymentMethod).toEqual({
  93. id: 'T_1',
  94. name: 'No Checker',
  95. code: 'no-checks',
  96. description: 'This is a test payment method',
  97. enabled: true,
  98. checker: null,
  99. handler: {
  100. args: [
  101. {
  102. name: 'automaticSettle',
  103. value: 'true',
  104. },
  105. ],
  106. code: 'dummy-payment-handler',
  107. },
  108. translations: [
  109. {
  110. description: 'This is a test payment method',
  111. id: 'T_1',
  112. languageCode: 'en',
  113. name: 'No Checker',
  114. },
  115. ],
  116. });
  117. });
  118. it('update', async () => {
  119. const { updatePaymentMethod } = await adminClient.query(updatePaymentMethodDocument, {
  120. input: {
  121. id: 'T_1',
  122. checker: {
  123. code: minPriceChecker.code,
  124. arguments: [{ name: 'minPrice', value: '0' }],
  125. },
  126. handler: {
  127. code: dummyPaymentHandler.code,
  128. arguments: [{ name: 'automaticSettle', value: 'false' }],
  129. },
  130. translations: [
  131. {
  132. languageCode: LanguageCode.en,
  133. description: 'modified',
  134. },
  135. ],
  136. },
  137. });
  138. expect(updatePaymentMethod).toEqual({
  139. id: 'T_1',
  140. name: 'No Checker',
  141. code: 'no-checks',
  142. description: 'modified',
  143. enabled: true,
  144. checker: {
  145. args: [{ name: 'minPrice', value: '0' }],
  146. code: minPriceChecker.code,
  147. },
  148. handler: {
  149. args: [
  150. {
  151. name: 'automaticSettle',
  152. value: 'false',
  153. },
  154. ],
  155. code: dummyPaymentHandler.code,
  156. },
  157. translations: [
  158. {
  159. description: 'modified',
  160. id: 'T_1',
  161. languageCode: 'en',
  162. name: 'No Checker',
  163. },
  164. ],
  165. });
  166. });
  167. it('unset checker', async () => {
  168. const { updatePaymentMethod } = await adminClient.query(updatePaymentMethodDocument, {
  169. input: {
  170. id: 'T_1',
  171. checker: null,
  172. },
  173. });
  174. expect(updatePaymentMethod.checker).toEqual(null);
  175. const { paymentMethod } = await adminClient.query(getPaymentMethodDocument, { id: 'T_1' });
  176. paymentMethodGuard.assertSuccess(paymentMethod);
  177. expect(paymentMethod.checker).toEqual(null);
  178. });
  179. it('paymentMethodEligibilityCheckers', async () => {
  180. const { paymentMethodEligibilityCheckers } = await adminClient.query(
  181. getPaymentMethodCheckersDocument,
  182. );
  183. expect(paymentMethodEligibilityCheckers).toEqual([
  184. {
  185. code: minPriceChecker.code,
  186. args: [{ name: 'minPrice', type: 'int' }],
  187. },
  188. ]);
  189. });
  190. it('paymentMethodHandlers', async () => {
  191. const { paymentMethodHandlers } = await adminClient.query(getPaymentMethodHandlersDocument);
  192. expect(paymentMethodHandlers).toEqual([
  193. {
  194. code: dummyPaymentHandler.code,
  195. args: [{ name: 'automaticSettle', type: 'boolean' }],
  196. },
  197. ]);
  198. });
  199. describe('eligibility checks', () => {
  200. beforeAll(async () => {
  201. await adminClient.query(createPaymentMethodDocument, {
  202. input: {
  203. code: 'price-check',
  204. enabled: true,
  205. checker: {
  206. code: minPriceChecker.code,
  207. arguments: [{ name: 'minPrice', value: '200000' }],
  208. },
  209. handler: {
  210. code: dummyPaymentHandler.code,
  211. arguments: [{ name: 'automaticSettle', value: 'true' }],
  212. },
  213. translations: [
  214. {
  215. languageCode: LanguageCode.en,
  216. name: 'With Min Price Checker',
  217. description: 'Order total must be more than 2k',
  218. },
  219. ],
  220. },
  221. });
  222. await adminClient.query(createPaymentMethodDocument, {
  223. input: {
  224. code: 'disabled-method',
  225. enabled: false,
  226. handler: {
  227. code: dummyPaymentHandler.code,
  228. arguments: [{ name: 'automaticSettle', value: 'true' }],
  229. },
  230. translations: [
  231. {
  232. languageCode: LanguageCode.en,
  233. name: 'Disabled ones',
  234. description: 'This method is disabled',
  235. },
  236. ],
  237. },
  238. });
  239. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  240. await shopClient.query(addItemToOrderDocument, {
  241. productVariantId: 'T_1',
  242. quantity: 1,
  243. });
  244. await proceedToArrangingPayment(shopClient);
  245. });
  246. it('eligiblePaymentMethods', async () => {
  247. const { eligiblePaymentMethods } = await shopClient.query(getEligiblePaymentMethodsDocument);
  248. expect(eligiblePaymentMethods).toEqual([
  249. {
  250. id: 'T_1',
  251. code: 'no-checks',
  252. isEligible: true,
  253. eligibilityMessage: null,
  254. },
  255. {
  256. id: 'T_2',
  257. code: 'price-check',
  258. isEligible: false,
  259. eligibilityMessage: 'Order total too low',
  260. },
  261. ]);
  262. });
  263. it('addPaymentToOrder does not allow ineligible method', async () => {
  264. checkerSpy.mockClear();
  265. const { addPaymentToOrder } = await shopClient.query(addPaymentDocument, {
  266. input: {
  267. method: 'price-check',
  268. metadata: {},
  269. },
  270. });
  271. orderGuard.assertErrorResult(addPaymentToOrder);
  272. expect(addPaymentToOrder.errorCode).toBe(ErrorCode.INELIGIBLE_PAYMENT_METHOD_ERROR);
  273. if ('eligibilityCheckerMessage' in addPaymentToOrder) {
  274. expect(addPaymentToOrder.eligibilityCheckerMessage).toBe('Order total too low');
  275. }
  276. expect(checkerSpy).toHaveBeenCalledTimes(1);
  277. });
  278. });
  279. describe('channels', () => {
  280. const SECOND_CHANNEL_TOKEN = 'SECOND_CHANNEL_TOKEN';
  281. const THIRD_CHANNEL_TOKEN = 'THIRD_CHANNEL_TOKEN';
  282. beforeAll(async () => {
  283. await adminClient.query(createChannelDocument, {
  284. input: {
  285. code: 'second-channel',
  286. token: SECOND_CHANNEL_TOKEN,
  287. defaultLanguageCode: LanguageCode.en,
  288. currencyCode: CurrencyCode.GBP,
  289. pricesIncludeTax: true,
  290. defaultShippingZoneId: 'T_1',
  291. defaultTaxZoneId: 'T_1',
  292. },
  293. });
  294. await adminClient.query(createChannelDocument, {
  295. input: {
  296. code: 'third-channel',
  297. token: THIRD_CHANNEL_TOKEN,
  298. defaultLanguageCode: LanguageCode.en,
  299. currencyCode: CurrencyCode.GBP,
  300. pricesIncludeTax: true,
  301. defaultShippingZoneId: 'T_1',
  302. defaultTaxZoneId: 'T_1',
  303. },
  304. });
  305. });
  306. it('creates a PaymentMethod in channel2', async () => {
  307. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  308. const { createPaymentMethod } = await adminClient.query(createPaymentMethodDocument, {
  309. input: {
  310. code: 'channel-2-method',
  311. enabled: true,
  312. handler: {
  313. code: dummyPaymentHandler.code,
  314. arguments: [{ name: 'automaticSettle', value: 'true' }],
  315. },
  316. translations: [
  317. {
  318. languageCode: LanguageCode.en,
  319. name: 'Channel 2 method',
  320. description: 'This is a test payment method',
  321. },
  322. ],
  323. },
  324. });
  325. expect(createPaymentMethod.code).toBe('channel-2-method');
  326. });
  327. it('method is listed in channel2', async () => {
  328. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  329. const { paymentMethods } = await adminClient.query(getPaymentMethodListDocument);
  330. expect(paymentMethods.totalItems).toBe(1);
  331. expect(paymentMethods.items[0].code).toBe('channel-2-method');
  332. });
  333. it('method is not listed in channel3', async () => {
  334. adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);
  335. const { paymentMethods } = await adminClient.query(getPaymentMethodListDocument);
  336. expect(paymentMethods.totalItems).toBe(0);
  337. });
  338. it('method is listed in default channel', async () => {
  339. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  340. const { paymentMethods } = await adminClient.query(getPaymentMethodListDocument);
  341. expect(paymentMethods.totalItems).toBe(4);
  342. expect(paymentMethods.items.map(i => i.code).sort()).toEqual([
  343. 'channel-2-method',
  344. 'disabled-method',
  345. 'no-checks',
  346. 'price-check',
  347. ]);
  348. });
  349. it('delete from channel', async () => {
  350. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  351. const { paymentMethods } = await adminClient.query(getPaymentMethodListDocument);
  352. expect(paymentMethods.totalItems).toBe(1);
  353. const { deletePaymentMethod } = await adminClient.query(deletePaymentMethodDocument, {
  354. id: paymentMethods.items[0].id,
  355. });
  356. expect(deletePaymentMethod.result).toBe(DeletionResult.DELETED);
  357. const { paymentMethods: checkChannel } = await adminClient.query(getPaymentMethodListDocument);
  358. expect(checkChannel.totalItems).toBe(0);
  359. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  360. const { paymentMethods: checkDefault } = await adminClient.query(getPaymentMethodListDocument);
  361. expect(checkDefault.totalItems).toBe(4);
  362. });
  363. it('delete from default channel', async () => {
  364. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  365. const { createPaymentMethod } = await adminClient.query(createPaymentMethodDocument, {
  366. input: {
  367. code: 'channel-2-method2',
  368. enabled: true,
  369. handler: {
  370. code: dummyPaymentHandler.code,
  371. arguments: [{ name: 'automaticSettle', value: 'true' }],
  372. },
  373. translations: [
  374. {
  375. languageCode: LanguageCode.en,
  376. name: 'Channel 2 method 2',
  377. description: 'This is a test payment method',
  378. },
  379. ],
  380. },
  381. });
  382. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  383. const { deletePaymentMethod: delete1 } = await adminClient.query(deletePaymentMethodDocument, {
  384. id: createPaymentMethod.id,
  385. });
  386. expect(delete1.result).toBe(DeletionResult.NOT_DELETED);
  387. expect(delete1.message).toBe(
  388. 'The selected PaymentMethod is assigned to the following Channels: second-channel. Set "force: true" to delete from all Channels.',
  389. );
  390. const { paymentMethods: check1 } = await adminClient.query(getPaymentMethodListDocument);
  391. expect(check1.totalItems).toBe(5);
  392. const { deletePaymentMethod: delete2 } = await adminClient.query(deletePaymentMethodDocument, {
  393. id: createPaymentMethod.id,
  394. force: true,
  395. });
  396. expect(delete2.result).toBe(DeletionResult.DELETED);
  397. const { paymentMethods: check2 } = await adminClient.query(getPaymentMethodListDocument);
  398. expect(check2.totalItems).toBe(4);
  399. });
  400. });
  401. it('create without description', async () => {
  402. const { createPaymentMethod } = await adminClient.query(createPaymentMethodDocument, {
  403. input: {
  404. code: 'no-description',
  405. enabled: true,
  406. handler: {
  407. code: dummyPaymentHandler.code,
  408. arguments: [{ name: 'automaticSettle', value: 'true' }],
  409. },
  410. translations: [
  411. {
  412. languageCode: LanguageCode.en,
  413. name: 'No Description',
  414. },
  415. ],
  416. },
  417. });
  418. expect(createPaymentMethod).toEqual({
  419. id: 'T_6',
  420. name: 'No Description',
  421. code: 'no-description',
  422. description: '',
  423. enabled: true,
  424. checker: null,
  425. handler: {
  426. args: [
  427. {
  428. name: 'automaticSettle',
  429. value: 'true',
  430. },
  431. ],
  432. code: 'dummy-payment-handler',
  433. },
  434. translations: [
  435. {
  436. description: '',
  437. id: 'T_6',
  438. languageCode: 'en',
  439. name: 'No Description',
  440. },
  441. ],
  442. });
  443. });
  444. it('returns only active payment methods', async () => {
  445. // Cleanup: Remove all existing payment methods
  446. const { paymentMethods } = await adminClient.query(getPaymentMethodListDocument);
  447. for (const method of paymentMethods.items) {
  448. await adminClient.query(deletePaymentMethodDocument, { id: method.id, force: true });
  449. }
  450. // Arrange: Create both enabled and disabled payment methods
  451. await adminClient.query(createPaymentMethodDocument, {
  452. input: {
  453. code: 'active-method',
  454. enabled: true,
  455. handler: {
  456. code: 'dummy-payment-handler',
  457. arguments: [{ name: 'automaticSettle', value: 'true' }],
  458. },
  459. translations: [
  460. {
  461. languageCode: LanguageCode.en,
  462. name: 'Active Method',
  463. description: 'This is an active method',
  464. },
  465. ],
  466. },
  467. });
  468. await adminClient.query(createPaymentMethodDocument, {
  469. input: {
  470. code: 'inactive-method',
  471. enabled: false,
  472. handler: {
  473. code: 'dummy-payment-handler',
  474. arguments: [{ name: 'automaticSettle', value: 'true' }],
  475. },
  476. translations: [
  477. {
  478. languageCode: LanguageCode.en,
  479. name: 'Inactive Method',
  480. description: 'This is an inactive method',
  481. },
  482. ],
  483. },
  484. });
  485. // Act: Query active payment methods
  486. const { activePaymentMethods } = await shopClient.query(activePaymentMethodsQueryDocument);
  487. // Assert: Ensure only the active payment method is returned
  488. expect(activePaymentMethods).toHaveLength(1);
  489. const activeMethod = activePaymentMethods[0];
  490. activePaymentMethodGuard.assertSuccess(activeMethod);
  491. expect(activeMethod.code).toBe('active-method');
  492. expect(activeMethod.name).toBe('Active Method');
  493. expect(activeMethod.description).toBe('This is an active method');
  494. });
  495. });
  496. export const paymentMethodFragment = graphql(`
  497. fragment PaymentMethod on PaymentMethod {
  498. id
  499. code
  500. name
  501. description
  502. enabled
  503. checker {
  504. code
  505. args {
  506. name
  507. value
  508. }
  509. }
  510. handler {
  511. code
  512. args {
  513. name
  514. value
  515. }
  516. }
  517. translations {
  518. id
  519. languageCode
  520. name
  521. description
  522. }
  523. }
  524. `);
  525. export const createPaymentMethodDocument = graphql(
  526. `
  527. mutation CreatePaymentMethod($input: CreatePaymentMethodInput!) {
  528. createPaymentMethod(input: $input) {
  529. ...PaymentMethod
  530. }
  531. }
  532. `,
  533. [paymentMethodFragment],
  534. );
  535. export const updatePaymentMethodDocument = graphql(
  536. `
  537. mutation UpdatePaymentMethod($input: UpdatePaymentMethodInput!) {
  538. updatePaymentMethod(input: $input) {
  539. ...PaymentMethod
  540. }
  541. }
  542. `,
  543. [paymentMethodFragment],
  544. );
  545. export const getPaymentMethodHandlersDocument = graphql(`
  546. query GetPaymentMethodHandlers {
  547. paymentMethodHandlers {
  548. code
  549. args {
  550. name
  551. type
  552. }
  553. }
  554. }
  555. `);
  556. export const getPaymentMethodCheckersDocument = graphql(`
  557. query GetPaymentMethodCheckers {
  558. paymentMethodEligibilityCheckers {
  559. code
  560. args {
  561. name
  562. type
  563. }
  564. }
  565. }
  566. `);
  567. export const getPaymentMethodDocument = graphql(
  568. `
  569. query GetPaymentMethod($id: ID!) {
  570. paymentMethod(id: $id) {
  571. ...PaymentMethod
  572. }
  573. }
  574. `,
  575. [paymentMethodFragment],
  576. );
  577. export const getPaymentMethodListDocument = graphql(
  578. `
  579. query GetPaymentMethodList($options: PaymentMethodListOptions) {
  580. paymentMethods(options: $options) {
  581. items {
  582. ...PaymentMethod
  583. }
  584. totalItems
  585. }
  586. }
  587. `,
  588. [paymentMethodFragment],
  589. );
  590. export const deletePaymentMethodDocument = graphql(`
  591. mutation DeletePaymentMethod($id: ID!, $force: Boolean) {
  592. deletePaymentMethod(id: $id, force: $force) {
  593. message
  594. result
  595. }
  596. }
  597. `);