diff --git a/packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts b/packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts index d7ccb46c24..7896828016 100644 --- a/packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts +++ b/packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts @@ -42,12 +42,15 @@ import { import { AddItemToOrderMutation, AddItemToOrderMutationVariables, + AdjustOrderLineMutation, + AdjustOrderLineMutationVariables, GetOrderByCodeQuery, GetOrderByCodeQueryVariables, TestOrderFragmentFragment, } from './graphql/generated-shop-types'; import { ADD_ITEM_TO_ORDER, + ADJUST_ORDER_LINE, APPLY_COUPON_CODE, GET_ACTIVE_ORDER, GET_ORDER_BY_CODE, @@ -60,6 +63,7 @@ import { GET_MOLLIE_PAYMENT_METHODS, refundOrderLine, setShipping, + testPaymentEligibilityChecker, } from './payment-helpers'; const mockData = { @@ -181,6 +185,9 @@ describe('Mollie payments', () => { beforeAll(async () => { const devConfig = mergeConfig(testConfig(), { plugins: [MolliePlugin.init({ vendureHost: mockData.host })], + paymentOptions: { + paymentMethodEligibilityCheckers: [testPaymentEligibilityChecker], + }, }); const env = createTestEnvironment(devConfig); serverPort = devConfig.apiOptions.port; @@ -224,6 +231,10 @@ describe('Mollie payments', () => { input: { code: mockData.methodCode, enabled: true, + checker: { + code: testPaymentEligibilityChecker.code, + arguments: [], + }, handler: { code: molliePaymentHandler.code, arguments: [ @@ -390,7 +401,41 @@ describe('Mollie payments', () => { }); }); + it('Should not allow creating intent if payment method is not eligible', async () => { + // Set quantity to 9, which is not allowe by our test eligibility checker + await shopClient.query( + ADJUST_ORDER_LINE, + { + orderLineId: order.lines[0].id, + quantity: 9, + }, + ); + let mollieRequest: any | undefined; + nock('https://api.mollie.com/') + .post('/v2/orders', body => { + mollieRequest = body; + return true; + }) + .reply(200, mockData.mollieOrderResponse); + const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, { + input: { + paymentMethodCode: mockData.methodCode, + redirectUrl: 'given-storefront-redirect-url', + }, + }); + expect(createMolliePaymentIntent.errorCode).toBe('INELIGIBLE_PAYMENT_METHOD_ERROR'); + expect(createMolliePaymentIntent.message).toContain('is not eligible for order'); + }); + it('Should get payment url with deducted amount if a payment is already made', async () => { + // Change quantity back to 10 + await shopClient.query( + ADJUST_ORDER_LINE, + { + orderLineId: order.lines[0].id, + quantity: 10, + }, + ); let mollieRequest: any | undefined; nock('https://api.mollie.com/') .post('/v2/orders', body => { @@ -702,7 +747,6 @@ describe('Mollie payments', () => { >(CREATE_PAYMENT_METHOD, { input: { code: mockData.methodCodeBroken, - enabled: true, handler: { code: molliePaymentHandler.code, diff --git a/packages/payments-plugin/e2e/payment-helpers.ts b/packages/payments-plugin/e2e/payment-helpers.ts index 24a5ac971a..55687b71b2 100644 --- a/packages/payments-plugin/e2e/payment-helpers.ts +++ b/packages/payments-plugin/e2e/payment-helpers.ts @@ -2,7 +2,9 @@ import { ID } from '@vendure/common/lib/shared-types'; import { ChannelService, ErrorResult, + LanguageCode, OrderService, + PaymentMethodEligibilityChecker, PaymentService, RequestContext, assertFound, @@ -189,6 +191,24 @@ export async function createFreeShippingCoupon( } } +/** + * Test payment eligibility checker that doesn't allow orders with quantity 9 on an order line, + * just so that we can easily mock non-eligibility + */ +export const testPaymentEligibilityChecker = new PaymentMethodEligibilityChecker({ + code: 'test-payment-eligibility-checker', + description: [{ languageCode: LanguageCode.en, value: 'Do not allow 9 items' }], + args: {}, + check: (ctx, order, args) => { + const hasLineWithQuantity9 = order.lines.find(line => line.quantity === 9); + if (hasLineWithQuantity9) { + return false; + } else { + return true; + } + }, +}); + export const CREATE_MOLLIE_PAYMENT_INTENT = gql` mutation createMolliePaymentIntent($input: MolliePaymentIntentInput!) { createMolliePaymentIntent(input: $input) { diff --git a/packages/payments-plugin/src/mollie/mollie.service.ts b/packages/payments-plugin/src/mollie/mollie.service.ts index 995fe7b495..a6ca3ac49c 100644 --- a/packages/payments-plugin/src/mollie/mollie.service.ts +++ b/packages/payments-plugin/src/mollie/mollie.service.ts @@ -13,6 +13,7 @@ import { EntityHydrator, ErrorResult, ID, + idsAreEqual, Injector, LanguageCode, Logger, @@ -94,16 +95,33 @@ export class MollieService { if (order instanceof PaymentIntentError) { return order; } - await this.entityHydrator.hydrate(ctx, order, { - relations: [ - 'customer', - 'surcharges', - 'lines.productVariant', - 'lines.productVariant.translations', - 'shippingLines.shippingMethod', - 'payments', - ], - }); + if (!paymentMethod) { + return new PaymentIntentError(`No paymentMethod found with code ${String(paymentMethodCode)}`); + } + const [eligiblePaymentMethods] = await Promise.all([ + this.orderService.getEligiblePaymentMethods(ctx, order.id), + await this.entityHydrator.hydrate(ctx, order, { + relations: [ + 'customer', + 'surcharges', + 'lines.productVariant', + 'lines.productVariant.translations', + 'shippingLines.shippingMethod', + 'payments', + ], + }), + ]); + if ( + !eligiblePaymentMethods.find( + eligibleMethod => + idsAreEqual(eligibleMethod.id, paymentMethod?.id) && eligibleMethod.isEligible, + ) + ) { + // Given payment method code is not eligible for this order + return new InvalidInputError( + `Payment method ${paymentMethod?.code} is not eligible for order ${order.code}`, + ); + } if (order.state !== 'ArrangingPayment' && order.state !== 'ArrangingAdditionalPayment') { // Pre-check if order is transitionable to ArrangingPayment, because that will happen after Mollie payment try { @@ -125,9 +143,6 @@ export class MollieService { 'Cannot create payment intent for order with customer that has no lastName set', ); } - if (!paymentMethod) { - return new PaymentIntentError(`No paymentMethod found with code ${String(paymentMethodCode)}`); - } let redirectUrl = input.redirectUrl; if (!redirectUrl) { // Use fallback redirect if no redirectUrl is given