Browse Source

Merge branch 'fork/feat/mollie-admin-payment' into minor

Michael Bromley 1 year ago
parent
commit
926895df73

+ 1 - 0
.gitignore

@@ -388,3 +388,4 @@ Icon
 Network Trash Folder
 Temporary Items
 .apdisk
+__admin-ui/

+ 65 - 16
package-lock.json

@@ -28873,18 +28873,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/stripe": {
-      "version": "14.20.0",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@types/node": ">=8.1.0",
-        "qs": "^6.11.0"
-      },
-      "engines": {
-        "node": ">=12.*"
-      }
-    },
     "node_modules/strnum": {
       "version": "1.0.5",
       "dev": true,
@@ -32345,10 +32333,10 @@
         "@vendure/testing": "2.2.0-next.5",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
-        "nock": "^13.5.4",
-        "rimraf": "^5.0.5",
-        "stripe": "^14.20.0",
-        "typescript": "5.3.3"
+        "nock": "^13.1.4",
+        "rimraf": "^3.0.2",
+        "stripe": "^13.3.0",
+        "typescript": "5.1.6"
       },
       "funding": {
         "url": "https://github.com/sponsors/michaelbromley"
@@ -32370,6 +32358,67 @@
         }
       }
     },
+    "packages/payments-plugin/node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "packages/payments-plugin/node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "dev": true,
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "packages/payments-plugin/node_modules/stripe": {
+      "version": "13.11.0",
+      "resolved": "https://registry.npmjs.org/stripe/-/stripe-13.11.0.tgz",
+      "integrity": "sha512-yPxVJxUzP1QHhHeFnYjJl48QwDS1+5befcL7ju7+t+i88D5r0rbsL+GkCCS6zgcU+TiV5bF9eMGcKyJfLf8BZQ==",
+      "dev": true,
+      "dependencies": {
+        "@types/node": ">=8.1.0",
+        "qs": "^6.11.0"
+      },
+      "engines": {
+        "node": ">=12.*"
+      }
+    },
+    "packages/payments-plugin/node_modules/typescript": {
+      "version": "5.1.6",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
+      "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
     "packages/sentry-plugin": {
       "name": "@vendure/sentry-plugin",
       "version": "2.2.0-next.5",

+ 9 - 12
packages/payments-plugin/e2e/mollie-dev-server.ts

@@ -3,21 +3,19 @@ import {
     ChannelService,
     DefaultLogger,
     DefaultSearchPlugin,
-    Logger,
     LogLevel,
     mergeConfig,
-    OrderService,
-    PaymentService,
     RequestContext,
 } from '@vendure/core';
 import { createTestEnvironment, registerInitializer, SqljsInitializer, testConfig } from '@vendure/testing';
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
 import gql from 'graphql-tag';
 import localtunnel from 'localtunnel';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { MolliePlugin } from '../package/mollie';
 import { molliePaymentHandler } from '../package/mollie/mollie.handler';
+import { MolliePlugin } from '../src/mollie';
 
 import { CREATE_PAYMENT_METHOD } from './graphql/admin-queries';
 import {
@@ -32,11 +30,10 @@ import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
 /**
  * This should only be used to locally test the Mollie payment plugin
  * Make sure you have `MOLLIE_APIKEY=test_xxxx` in your .env file
+ * Make sure you have `MOLLIE_APIKEY=test_xxxx` in your .env file
  */
 /* eslint-disable @typescript-eslint/no-floating-promises */
-async function runMollieDevServer(useDynamicRedirectUrl: boolean) {
-    // eslint-disable-next-line no-console
-    console.log('Starting Mollie dev server with dynamic redirectUrl: ', useDynamicRedirectUrl);
+async function runMollieDevServer() {
     // eslint-disable-next-line @typescript-eslint/no-var-requires
     require('dotenv').config();
 
@@ -50,7 +47,7 @@ async function runMollieDevServer(useDynamicRedirectUrl: boolean) {
                 route: 'admin',
                 port: 5001,
             }),
-            MolliePlugin.init({ vendureHost: tunnel.url, useDynamicRedirectUrl }),
+            MolliePlugin.init({ vendureHost: tunnel.url }),
         ],
         logger: new DefaultLogger({ level: LogLevel.Debug }),
         apiOptions: {
@@ -92,7 +89,7 @@ async function runMollieDevServer(useDynamicRedirectUrl: boolean) {
                     arguments: [
                         {
                             name: 'redirectUrl',
-                            value: `${tunnel.url}/admin/orders?filter=open&page=1&dynamicRedirectUrl=false`,
+                            value: `${tunnel.url}/admin/orders?filter=open&page=1`,
                         },
                         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                         { name: 'apiKey', value: process.env.MOLLIE_APIKEY! },
@@ -115,9 +112,10 @@ async function runMollieDevServer(useDynamicRedirectUrl: boolean) {
     });
     await setShipping(shopClient);
     // Create payment intent
+    // Create payment intent
     const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
         input: {
-            redirectUrl: `${tunnel.url}/admin/orders?filter=open&page=1&dynamicRedirectUrl=true`,
+            redirectUrl: `${tunnel.url}/admin/orders?filter=open&page=1`,
             paymentMethodCode: 'mollie',
             //            molliePaymentMethodCode: 'klarnapaylater'
         },
@@ -148,6 +146,5 @@ async function runMollieDevServer(useDynamicRedirectUrl: boolean) {
 }
 
 (async () => {
-    // Change the value of the parameter to true to test with the dynamic redirectUrl functionality
-    await runMollieDevServer(false);
+    await runMollieDevServer();
 })();

+ 69 - 228
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -55,7 +55,7 @@ import {
 
 const mockData = {
     host: 'https://my-vendure.io',
-    redirectUrl: 'https://my-storefront/order',
+    redirectUrl: 'https://fallback-redirect/order',
     apiKey: 'myApiKey',
     methodCode: `mollie-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`,
     methodCodeBroken: `mollie-payment-broken-${E2E_DEFAULT_CHANNEL_TOKEN}`,
@@ -139,7 +139,7 @@ let order: TestOrderFragmentFragment;
 let serverPort: number;
 const SURCHARGE_AMOUNT = -20000;
 
-describe('Mollie payments with useDynamicRedirectUrl=false', () => {
+describe('Mollie payments', () => {
     beforeAll(async () => {
         const devConfig = mergeConfig(testConfig(), {
             plugins: [MolliePlugin.init({ vendureHost: mockData.host })],
@@ -169,11 +169,11 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
         await server.destroy();
     });
 
-    afterEach(async () => {
+    afterEach(() => {
         nock.cleanAll();
     });
 
-    it('Should start successfully', async () => {
+    it('Should start successfully', () => {
         expect(started).toEqual(true);
         expect(customers).toHaveLength(2);
     });
@@ -299,13 +299,14 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
             const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
                 input: {
                     paymentMethodCode: mockData.methodCode,
+                    redirectUrl: 'given-storefront-redirect-url',
                 },
             });
             expect(createMolliePaymentIntent).toEqual({
                 url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
             });
             expect(mollieRequest?.orderNumber).toEqual(order.code);
-            expect(mollieRequest?.redirectUrl).toEqual(`${mockData.redirectUrl}/${order.code}`);
+            expect(mollieRequest?.redirectUrl).toEqual('given-storefront-redirect-url');
             expect(mollieRequest?.webhookUrl).toEqual(
                 `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
             );
@@ -320,6 +321,22 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
             expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
         });
 
+        it('Should use fallback redirect appended with order code, when no redirect is given', async () => {
+            let mollieRequest: any | undefined;
+            nock('https://api.mollie.com/')
+                .post('/v2/orders', body => {
+                    mollieRequest = body;
+                    return true;
+                })
+                .reply(200, mockData.mollieOrderResponse);
+            await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
+                input: {
+                    paymentMethodCode: mockData.methodCode,
+                },
+            });
+            expect(mollieRequest?.redirectUrl).toEqual(`${mockData.redirectUrl}/${order.code}`);
+        });
+
         it('Should get payment url with Mollie method', async () => {
             nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
             await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
@@ -342,8 +359,8 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
                 .reply(200, mockData.mollieOrderResponse);
             // Should patch existing order
             nock('https://api.mollie.com/')
-            .patch(`/v2/orders/${mockData.mollieOrderResponse.id}`)
-            .reply(200, mockData.mollieOrderResponse);
+                .patch(`/v2/orders/${mockData.mollieOrderResponse.id}`)
+                .reply(200, mockData.mollieOrderResponse);
             // Should patch existing order lines
             let molliePatchRequest: any | undefined;
             nock('https://api.mollie.com/')
@@ -394,6 +411,20 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
             expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
         });
 
+        it('Should create intent as admin', async () => {
+            nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
+            // Admin API passes order ID, and no payment method code
+            const { createMolliePaymentIntent: intent } = await adminClient.query(
+                CREATE_MOLLIE_PAYMENT_INTENT,
+                {
+                    input: {
+                        orderId: '1',
+                    },
+                },
+            );
+            expect(intent.url).toBe(mockData.mollieOrderResponse._links.checkout.href);
+        });
+
         it('Should get available paymentMethods', async () => {
             nock('https://api.mollie.com/')
                 .get('/v2/methods?resource=orders')
@@ -466,7 +497,7 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
             expect(order.state).toBe('PaymentSettled');
         });
 
-        it('Should have preserved original languageCode ', async () => {
+        it('Should have preserved original languageCode ', () => {
             // We've set the languageCode to 'nl' in the mock response's metadata
             expect(orderPlacedEvent?.ctx.languageCode).toBe('nl');
         });
@@ -603,228 +634,38 @@ describe('Mollie payments with useDynamicRedirectUrl=false', () => {
             expect(createShipmentBody).toBeDefined();
             expect(order.state).toBe('PaymentSettled');
         });
-    });
-
-    it('Should add an unusable Mollie paymentMethod (missing redirectUrl)', async () => {
-        const { createPaymentMethod } = await adminClient.query<
-            CreatePaymentMethodMutation,
-            CreatePaymentMethodMutationVariables
-        >(CREATE_PAYMENT_METHOD, {
-            input: {
-                code: mockData.methodCodeBroken,
-
-                enabled: true,
-                handler: {
-                    code: molliePaymentHandler.code,
-                    arguments: [
-                        { name: 'apiKey', value: mockData.apiKey },
-                        { name: 'autoCapture', value: 'false' },
-                    ],
-                },
-                translations: [
-                    {
-                        languageCode: LanguageCode.en,
-                        name: 'Mollie payment test',
-                        description: 'This is a Mollie test payment method',
-                    },
-                ],
-            },
-        });
-        expect(createPaymentMethod.code).toBe(mockData.methodCodeBroken);
-    });
-
-    it('Should prepare an order', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { addItemToOrder } = await shopClient.query<
-            AddItemToOrderMutation,
-            AddItemToOrderMutationVariables
-        >(ADD_ITEM_TO_ORDER, {
-            productVariantId: 'T_5',
-            quantity: 10,
-        });
-        order = addItemToOrder as TestOrderFragmentFragment;
-        // Add surcharge
-        const ctx = new RequestContext({
-            apiType: 'admin',
-            isAuthorized: true,
-            authorizedAsOwnerOnly: false,
-            channel: await server.app.get(ChannelService).getDefaultChannel(),
-        });
-        await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
-            description: 'Negative test surcharge',
-            listPrice: SURCHARGE_AMOUNT,
-        });
-        expect(order.code).toBeDefined();
-    });
-
-    it('Should fail to get payment url with Mollie method without redirectUrl configured', async () => {
-        nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        await setShipping(shopClient);
-        const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCodeBroken,
-                molliePaymentMethodCode: 'ideal',
-            },
-        });
-        expect(createMolliePaymentIntent.message).toContain(
-            'Cannot create payment intent without redirectUrl specified in paymentMethod',
-        );
-    });
-});
-
-describe('Mollie payments with useDynamicRedirectUrl=true', () => {
-    beforeAll(async () => {
-        const devConfig = mergeConfig(testConfig(), {
-            plugins: [MolliePlugin.init({ vendureHost: mockData.host, useDynamicRedirectUrl: true })],
-        });
-        const env = createTestEnvironment(devConfig);
-        serverPort = devConfig.apiOptions.port;
-        shopClient = env.shopClient;
-        adminClient = env.adminClient;
-        server = env.server;
-        await server.init({
-            initialData,
-            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
-            customerCount: 2,
-        });
-        started = true;
-        await adminClient.asSuperAdmin();
-        ({
-            customers: { items: customers },
-        } = await adminClient.query<GetCustomerListQuery, GetCustomerListQueryVariables>(GET_CUSTOMER_LIST, {
-            options: {
-                take: 2,
-            },
-        }));
-    }, TEST_SETUP_TIMEOUT_MS);
 
-    afterAll(async () => {
-        await server.destroy();
-    });
-
-    afterEach(async () => {
-        nock.cleanAll();
-    });
-
-    it('Should start successfully', async () => {
-        expect(started).toEqual(true);
-        expect(customers).toHaveLength(2);
-    });
-
-    it('Should prepare an order', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { addItemToOrder } = await shopClient.query<
-            AddItemToOrderMutation,
-            AddItemToOrderMutationVariables
-        >(ADD_ITEM_TO_ORDER, {
-            productVariantId: 'T_5',
-            quantity: 10,
-        });
-        order = addItemToOrder as TestOrderFragmentFragment;
-        // Add surcharge
-        const ctx = new RequestContext({
-            apiType: 'admin',
-            isAuthorized: true,
-            authorizedAsOwnerOnly: false,
-            channel: await server.app.get(ChannelService).getDefaultChannel(),
-        });
-        await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
-            description: 'Negative test surcharge',
-            listPrice: SURCHARGE_AMOUNT,
-        });
-        expect(order.code).toBeDefined();
-    });
-
-    it('Should add a working Mollie paymentMethod without specifying redirectUrl', async () => {
-        const { createPaymentMethod } = await adminClient.query<
-            CreatePaymentMethodMutation,
-            CreatePaymentMethodMutationVariables
-        >(CREATE_PAYMENT_METHOD, {
-            input: {
-                code: mockData.methodCode,
-                enabled: true,
-                handler: {
-                    code: molliePaymentHandler.code,
-                    arguments: [
-                        { name: 'apiKey', value: mockData.apiKey },
-                        { name: 'autoCapture', value: 'false' },
-                    ],
-                },
-                translations: [
-                    {
-                        languageCode: LanguageCode.en,
-                        name: 'Mollie payment test',
-                        description: 'This is a Mollie test payment method',
+        it('Should fail to add payment method without redirect url', async () => {
+            let error = '';
+            try {
+                const { createPaymentMethod } = await adminClient.query<
+                    CreatePaymentMethodMutation,
+                    CreatePaymentMethodMutationVariables
+                >(CREATE_PAYMENT_METHOD, {
+                    input: {
+                        code: mockData.methodCodeBroken,
+
+                        enabled: true,
+                        handler: {
+                            code: molliePaymentHandler.code,
+                            arguments: [
+                                { name: 'apiKey', value: mockData.apiKey },
+                                { name: 'autoCapture', value: 'false' },
+                            ],
+                        },
+                        translations: [
+                            {
+                                languageCode: LanguageCode.en,
+                                name: 'Mollie payment test',
+                                description: 'This is a Mollie test payment method',
+                            },
+                        ],
                     },
-                ],
-            },
-        });
-        expect(createPaymentMethod.code).toBe(mockData.methodCode);
-    });
-
-    it('Should get payment url without Mollie method', async () => {
-        await setShipping(shopClient);
-        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: mockData.redirectUrl,
-            },
-        });
-        expect(createMolliePaymentIntent).toEqual({
-            url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
-        });
-        expect(mollieRequest?.orderNumber).toEqual(order.code);
-        expect(mollieRequest?.redirectUrl).toEqual(mockData.redirectUrl);
-        expect(mollieRequest?.webhookUrl).toEqual(
-            `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
-        );
-        expect(mollieRequest?.amount?.value).toBe('1009.90');
-        expect(mollieRequest?.amount?.currency).toBe('USD');
-        expect(mollieRequest.lines[0].vatAmount.value).toEqual('199.98');
-        let totalLineAmount = 0;
-        for (const line of mollieRequest.lines) {
-            totalLineAmount += Number(line.totalAmount.value);
-        }
-        // Sum of lines should equal order total
-        expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
-    });
-
-    it('Should get payment url with Mollie method', async () => {
-        nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        await setShipping(shopClient);
-        const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-                molliePaymentMethodCode: 'ideal',
-                redirectUrl: mockData.redirectUrl,
-            },
-        });
-        expect(createMolliePaymentIntent).toEqual({
-            url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
-        });
-    });
-
-    it('Should fail to get payment url without specifying redirectUrl in the createMolliePaymentIntent mutation', async () => {
-        nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        await setShipping(shopClient);
-        const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-                molliePaymentMethodCode: 'ideal',
-            },
+                });
+            } catch (e: any) {
+                error = e.message;
+            }
+            expect(error).toBe('The argument "redirectUrl" is required');
         });
-        expect(createMolliePaymentIntent.message).toContain(
-            'Cannot create payment intent without redirectUrl specified',
-        );
     });
 });

+ 4 - 4
packages/payments-plugin/package.json

@@ -51,9 +51,9 @@
         "@vendure/testing": "2.2.0-next.5",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
-        "nock": "^13.5.4",
-        "rimraf": "^5.0.5",
-        "stripe": "^14.20.0",
-        "typescript": "5.3.3"
+        "nock": "^13.1.4",
+        "rimraf": "^3.0.2",
+        "stripe": "^13.3.0",
+        "typescript": "5.1.6"
     }
 }

+ 54 - 13
packages/payments-plugin/src/mollie/mollie-shop-schema.ts → packages/payments-plugin/src/mollie/api-extensions.ts

@@ -1,10 +1,52 @@
 import { gql } from 'graphql-tag';
 
-export const shopSchema = gql`
+const commonSchemaExtensions = gql`
+
+    input MolliePaymentIntentInput {
+        """
+        The code of the Vendure payment method to use for the payment.
+        Must have Mollie as payment method handler.
+        Without this, the first method with Mollie as handler will be used.
+        """
+        paymentMethodCode: String
+        """
+        The redirect url to which the customer will be redirected after the payment is completed.
+        The configured fallback redirect will be used if this is not provided.
+        """
+        redirectUrl: String
+        """
+        Optional preselected Mollie payment method. When this is passed
+        the payment selection step will be skipped.
+        """
+        molliePaymentMethodCode: String
+        """
+        Use this to create a payment intent for a specific order. This allows you to create intents for
+        orders that are not active orders.
+        """
+        orderId: String
+    }
+
+    type MolliePaymentIntent {
+        url: String!
+    }
+
     type MolliePaymentIntentError implements ErrorResult {
         errorCode: ErrorCode!
         message: String!
     }
+
+    union MolliePaymentIntentResult = MolliePaymentIntent | MolliePaymentIntentError
+
+    extend type Mutation {
+        createMolliePaymentIntent(input: MolliePaymentIntentInput!): MolliePaymentIntentResult!
+    }
+
+`;
+
+export const shopApiExtensions = gql`
+
+   ${commonSchemaExtensions}
+
     type MollieAmount {
         value: String
         currency: String
@@ -23,22 +65,21 @@ export const shopSchema = gql`
         image: MolliePaymentMethodImages
         status: String
     }
-    type MolliePaymentIntent {
-        url: String!
-    }
-    union MolliePaymentIntentResult = MolliePaymentIntent | MolliePaymentIntentError
-    input MolliePaymentIntentInput {
-        redirectUrl: String
-        paymentMethodCode: String!
-        molliePaymentMethodCode: String
-    }
+
     input MolliePaymentMethodsInput {
         paymentMethodCode: String!
     }
-    extend type Mutation {
-        createMolliePaymentIntent(input: MolliePaymentIntentInput!): MolliePaymentIntentResult!
-    }
+
     extend type Query {
         molliePaymentMethods(input: MolliePaymentMethodsInput!): [MolliePaymentMethod!]!
     }
 `;
+
+export const adminApiExtensions = gql`
+    
+        ${commonSchemaExtensions}
+
+        extend enum ErrorCode {
+            ORDER_PAYMENT_STATE_ERROR
+        }
+`;

File diff suppressed because it is too large
+ 578 - 565
packages/payments-plugin/src/mollie/graphql/generated-shop-types.ts


+ 3 - 14
packages/payments-plugin/src/mollie/mollie.resolver.ts → packages/payments-plugin/src/mollie/mollie.common-resolver.ts

@@ -1,18 +1,16 @@
-import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql';
+import { Args, Mutation, ResolveField, Resolver } from '@nestjs/graphql';
 import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
 
 import {
     MolliePaymentIntent,
     MolliePaymentIntentError,
     MolliePaymentIntentInput,
-    MolliePaymentIntentResult,
-    MolliePaymentMethod,
-    MolliePaymentMethodsInput,
+    MolliePaymentIntentResult
 } from './graphql/generated-shop-types';
 import { MollieService } from './mollie.service';
 
 @Resolver()
-export class MollieResolver {
+export class MollieCommonResolver {
     constructor(private mollieService: MollieService) {}
 
     @Mutation()
@@ -33,13 +31,4 @@ export class MollieResolver {
             return 'MolliePaymentIntent';
         }
     }
-
-    @Query()
-    @Allow(Permission.Public)
-    async molliePaymentMethods(
-        @Ctx() ctx: RequestContext,
-        @Args('input') { paymentMethodCode }: MolliePaymentMethodsInput,
-    ): Promise<MolliePaymentMethod[]> {
-        return this.mollieService.getEnabledPaymentMethods(ctx, paymentMethodCode);
-    }
 }

+ 13 - 5
packages/payments-plugin/src/mollie/mollie.handler.ts

@@ -43,13 +43,16 @@ export const molliePaymentHandler = new PaymentMethodHandler({
         },
         redirectUrl: {
             type: 'string',
-            required: false,
+            required: true,
             defaultValue: '',
-            label: [{ languageCode: LanguageCode.en, value: 'Redirect URL' }],
+            label: [{ languageCode: LanguageCode.en, value: 'Fallback redirect URL' }],
             description: [
-                { languageCode: LanguageCode.en, value: 'Redirect the client to this URL after payment' },
+                {
+                    languageCode: LanguageCode.en,
+                    value: 'Redirect URL to use when no URL is given by the storefront. Order code is appended to this URL',
+                },
             ],
-        }
+        },
     },
     init(injector) {
         mollieService = injector.get(MollieService);
@@ -72,7 +75,12 @@ export const molliePaymentHandler = new PaymentMethodHandler({
                 }. Only Authorized or Settled are allowed.`,
             );
         }
-        Logger.info(`Payment for order ${order.code} with amount ${metadata.amount} created with state '${metadata.status}'`, loggerCtx);
+        Logger.info(
+            `Payment for order ${order.code} with amount ${metadata.amount as string} created with state '${
+                metadata.status as string
+            }'`,
+            loggerCtx,
+        );
         return {
             amount: metadata.amount,
             state: metadata.status,

+ 17 - 34
packages/payments-plugin/src/mollie/mollie.plugin.ts

@@ -8,13 +8,14 @@ import {
     VendurePlugin,
 } from '@vendure/core';
 
+import { shopApiExtensions, adminApiExtensions } from './api-extensions';
 import { PLUGIN_INIT_OPTIONS } from './constants';
 import { orderCustomFields } from './custom-fields';
-import { shopSchema } from './mollie-shop-schema';
+import { MollieCommonResolver } from './mollie.common-resolver';
 import { MollieController } from './mollie.controller';
 import { molliePaymentHandler } from './mollie.handler';
-import { MollieResolver } from './mollie.resolver';
 import { MollieService } from './mollie.service';
+import { MollieShopResolver } from './mollie.shop-resolver';
 
 export type AdditionalEnabledPaymentMethodsParams = Partial<Omit<ListParameters, 'resource'>>;
 
@@ -33,18 +34,6 @@ export interface MolliePluginOptions {
      */
     vendureHost: string;
 
-    /**
-     * @description
-     * For backwards compatibility, by default set to false.
-     * This option will be deprecated in a future version.
-     * When enabled, the `redirectUrl` can be passed via the `createPaymentIntent` mutation
-     * instead of being configured in the Payment Method.
-     *
-     * @default false
-     * @since 2.0.0
-     */
-    useDynamicRedirectUrl?: boolean;
-
     /**
      * @description
      * Provide additional parameters to the Mollie enabled payment methods API call. By default,
@@ -113,34 +102,25 @@ export interface MolliePluginOptions {
  *     // ...
  *
  *     plugins: [
- *       MolliePlugin.init({ vendureHost: 'https://yourhost.io/', useDynamicRedirectUrl: true }),
+ *       MolliePlugin.init({ vendureHost: 'https://yourhost.io/' }),
  *     ]
  *     ```
  * 2. Run a database migration to add the `mollieOrderId` custom field to the order entity.
  * 3. Create a new PaymentMethod in the Admin UI, and select "Mollie payments" as the handler.
  * 4. Set your Mollie apiKey in the `API Key` field.
- *
- * ## Specifying the redirectUrl
- *
- * Currently, there are two ways to specify the `redirectUrl` to which the customer is redirected after completing the payment:
- * 1. Configure the `redirectUrl` in the PaymentMethod.
- * 2. Pass the `redirectUrl` as an argument to the `createPaymentIntent` mutation.
- *
- * Which method is used depends on the value of the `useDynamicRedirectUrl` option while initializing the plugin.
- * By default, this option is set to `false` for backwards compatibility. In a future version, this option will be deprecated.
- * Upon deprecation, the `redirectUrl` will always be passed as an argument to the `createPaymentIntent` mutation.
+ * 5. Set the `Fallback redirectUrl` to the url that the customer should be redirected to after completing the payment.
+ * You can override this url by passing the `redirectUrl` as an argument to the `createMolliePaymentIntent` mutation.
  *
  * ## Storefront usage
  *
  * In your storefront you add a payment to an order using the `createMolliePaymentIntent` mutation. In this example, our Mollie
  * PaymentMethod was given the code "mollie-payment-method". The `redirectUrl``is the url that is used to redirect the end-user
- * back to your storefront after completing the payment. When using the first method specified in `Specifying the redirectUrl`,
- * the order code is appened to the `redirectUrl`. For the second method, the order code is not appended to the specified `redirectUrl`.
+ * back to your storefront after completing the payment.
  *
  * ```GraphQL
  * mutation CreateMolliePaymentIntent {
  *   createMolliePaymentIntent(input: {
- *     redirectUrl: "https://storefront/order"
+ *     redirectUrl: "https://storefront/order/1234XYZ"
  *     paymentMethodCode: "mollie-payment-method"
  *     molliePaymentMethodCode: "ideal"
  *   }) {
@@ -184,10 +164,10 @@ export interface MolliePluginOptions {
  *  }
  * }
  * ```
- * You can pass `MolliePaymentMethod.code` to the `createMolliePaymentIntent` mutation to skip the method selection.
+ * You can pass `creditcard` for example, to the `createMolliePaymentIntent` mutation to skip the method selection.
  *
  * After completing payment on the Mollie platform,
- * the user is redirected to the configured redirect url + orderCode: `https://storefront/order/CH234X5`
+ * the user is redirected to the given redirect url, e.g. `https://storefront/order/CH234X5`
  *
  * ## Pay later methods
  * Mollie supports pay-later methods like 'Klarna Pay Later'. For pay-later methods, the status of an order is
@@ -219,10 +199,14 @@ export interface MolliePluginOptions {
         return config;
     },
     shopApiExtensions: {
-        schema: shopSchema,
-        resolvers: [MollieResolver],
+        schema: shopApiExtensions,
+        resolvers: [MollieCommonResolver, MollieShopResolver],
+    },
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [MollieCommonResolver],
     },
-    compatibility: '^2.0.0',
+    compatibility: '^2.2.0',
 })
 export class MolliePlugin {
     static options: MolliePluginOptions;
@@ -231,7 +215,6 @@ export class MolliePlugin {
      * @description
      * Initialize the mollie payment plugin
      * @param vendureHost is needed to pass to mollie for callback
-     * @param useDynamicRedirectUrl to indicate if the redirectUrl can be passed via the `createPaymentIntent` mutation, versus being configured in the Payment Method.
      */
     static init(options: MolliePluginOptions): typeof MolliePlugin {
         this.options = options;

+ 45 - 27
packages/payments-plugin/src/mollie/mollie.service.ts

@@ -30,6 +30,7 @@ import { totalCoveredByPayments } from '@vendure/core/dist/service/helpers/utils
 
 import { loggerCtx, PLUGIN_INIT_OPTIONS } from './constants';
 import { OrderWithMollieReference } from './custom-fields';
+import { createExtendedMollieClient, ExtendedMollieClient, ManageOrderLineInput } from './extended-mollie-client';
 import {
     ErrorCode,
     MolliePaymentIntentError,
@@ -37,6 +38,7 @@ import {
     MolliePaymentIntentResult,
     MolliePaymentMethod,
 } from './graphql/generated-shop-types';
+import { molliePaymentHandler } from './mollie.handler';
 import {
     amountToCents,
     areOrderLinesEqual,
@@ -46,7 +48,6 @@ import {
     toMollieOrderLines,
 } from './mollie.helpers';
 import { MolliePluginOptions } from './mollie.plugin';
-import { createExtendedMollieClient, ExtendedMollieClient, ManageOrderLineInput } from './extended-mollie-client';
 
 interface OrderStatusInput {
     paymentMethodId: string;
@@ -89,7 +90,6 @@ export class MollieService {
         input: MolliePaymentIntentInput,
     ): Promise<MolliePaymentIntentResult> {
         const { paymentMethodCode, molliePaymentMethodCode } = input;
-        let redirectUrl: string;
         const allowedMethods = Object.values(MollieClientMethod) as string[];
         if (molliePaymentMethodCode && !allowedMethods.includes(molliePaymentMethodCode)) {
             return new InvalidInputError(
@@ -97,11 +97,11 @@ export class MollieService {
             );
         }
         const [order, paymentMethod] = await Promise.all([
-            this.activeOrderService.getActiveOrder(ctx, undefined),
+            this.getOrder(ctx, input.orderId),
             this.getPaymentMethod(ctx, paymentMethodCode),
         ]);
-        if (!order) {
-            return new PaymentIntentError('No active order found for session');
+        if (order instanceof PaymentIntentError) {
+            return order;
         }
         await this.entityHydrator.hydrate(ctx, order, {
             relations: [
@@ -134,23 +134,23 @@ export class MollieService {
             );
         }
         if (!paymentMethod) {
-            return new PaymentIntentError(`No paymentMethod found with code ${paymentMethodCode}`);
+            return new PaymentIntentError(`No paymentMethod found with code ${String(paymentMethodCode)}`);
         }
-        if (this.options.useDynamicRedirectUrl === true) {
-            if (!input.redirectUrl) {
-                return new InvalidInputError('Cannot create payment intent without redirectUrl specified');
-            }
-            redirectUrl = input.redirectUrl;
-        } else {
-            const paymentMethodRedirectUrl = paymentMethod.handler.args.find(
-                arg => arg.name === 'redirectUrl',
-            )?.value;
-            if (!paymentMethodRedirectUrl) {
+        let redirectUrl = input.redirectUrl;
+        if (!redirectUrl) {
+            // Use fallback redirect if no redirectUrl is given
+            let fallbackRedirect = paymentMethod.handler.args.find(arg => arg.name === 'redirectUrl')?.value;
+            if (!fallbackRedirect) {
                 return new PaymentIntentError(
-                    'Cannot create payment intent without redirectUrl specified in paymentMethod',
+                    'No redirect URl was given and no fallback redirect is configured',
                 );
             }
-            redirectUrl = paymentMethodRedirectUrl;
+            redirectUrl = fallbackRedirect;
+            // remove appending slash if present
+            fallbackRedirect = fallbackRedirect.endsWith('/')
+                ? fallbackRedirect.slice(0, -1)
+                : fallbackRedirect;
+            redirectUrl = `${fallbackRedirect}/${order.code}`;
         }
         const apiKey = paymentMethod.handler.args.find(arg => arg.name === 'apiKey')?.value;
         if (!apiKey) {
@@ -161,10 +161,6 @@ export class MollieService {
             return new PaymentIntentError(`Paymentmethod ${paymentMethod.code} has no apiKey configured`);
         }
         const mollieClient = createExtendedMollieClient({ apiKey });
-        redirectUrl =
-            redirectUrl.endsWith('/') && this.options.useDynamicRedirectUrl !== true
-                ? redirectUrl.slice(0, -1)
-                : redirectUrl; // remove appending slash
         const vendureHost = this.options.vendureHost.endsWith('/')
             ? this.options.vendureHost.slice(0, -1)
             : this.options.vendureHost; // remove appending slash
@@ -182,8 +178,7 @@ export class MollieService {
         const orderInput: CreateParameters = {
             orderNumber: order.code,
             amount: toAmount(amountToPay, order.currencyCode),
-            redirectUrl:
-                this.options.useDynamicRedirectUrl === true ? redirectUrl : `${redirectUrl}/${order.code}`,
+            redirectUrl,
             webhookUrl: `${vendureHost}/payments/mollie/${ctx.channel.token}/${paymentMethod.id}`,
             billingAddress,
             locale: getLocale(billingAddress.country, ctx.languageCode),
@@ -510,9 +505,32 @@ export class MollieService {
 
     private async getPaymentMethod(
         ctx: RequestContext,
-        paymentMethodCode: string,
+        paymentMethodCode?: string | null,
     ): Promise<PaymentMethod | undefined> {
-        const paymentMethods = await this.paymentMethodService.findAll(ctx);
-        return paymentMethods.items.find(pm => pm.code === paymentMethodCode);
+        if (paymentMethodCode) {
+            const { items } = await this.paymentMethodService.findAll(ctx, {
+                filter: {
+                    code: { eq: paymentMethodCode },
+                },
+            });
+            return items.find(pm => pm.code === paymentMethodCode);
+        } else {
+            const { items } = await this.paymentMethodService.findAll(ctx);
+            return items.find(pm => pm.handler.code === molliePaymentHandler.code);
+        }
+    }
+
+    /**
+     * Get order by id, or active order if no orderId is given
+     */
+    private async getOrder(ctx: RequestContext, orderId?: ID | null): Promise<Order | PaymentIntentError> {
+        if (orderId) {
+            return await assertFound(this.orderService.findOne(ctx, orderId));
+        }
+        const order = await this.activeOrderService.getActiveOrder(ctx, undefined);
+        if (!order) {
+            return new PaymentIntentError('No active order found for session');
+        }
+        return order;
     }
 }

+ 22 - 0
packages/payments-plugin/src/mollie/mollie.shop-resolver.ts

@@ -0,0 +1,22 @@
+import { Args, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, Permission, RequestContext } from '@vendure/core';
+
+import {
+    MolliePaymentMethod,
+    MolliePaymentMethodsInput
+} from './graphql/generated-shop-types';
+import { MollieService } from './mollie.service';
+
+@Resolver()
+export class MollieShopResolver {
+    constructor(private mollieService: MollieService) {}
+
+    @Query()
+    @Allow(Permission.Public)
+    async molliePaymentMethods(
+        @Ctx() ctx: RequestContext,
+        @Args('input') { paymentMethodCode }: MolliePaymentMethodsInput,
+    ): Promise<MolliePaymentMethod[]> {
+        return this.mollieService.getEnabledPaymentMethods(ctx, paymentMethodCode);
+    }
+}

Some files were not shown because too many files changed in this diff