Browse Source

Merge branch 'master' into minor

Michael Bromley 2 years ago
parent
commit
564eff6a6b

+ 5 - 0
CHANGELOG.md

@@ -1,3 +1,8 @@
+## <small>1.9.5 (2023-03-24)</small>
+
+#### Fixes 
+* **payments-plugin** Fix issue with handling of partial payments in Mollie. If you are using the MolliePlugin you should update as a priority. ([#2092](https://github.com/vendure-ecommerce/vendure/pull/2092))
+
 ## <small>1.9.4 (2023-03-22)</small>
 
 

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "1.9.4",
+  "version": "1.9.5",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -21,8 +21,8 @@
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -39,7 +39,7 @@
     "@ng-select/ng-select": "^7.2.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^1.9.4",
+    "@vendure/common": "^1.9.5",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.6.0",
     "apollo-upload-client": "^16.0.0",

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '1.9.4';
+export const ADMIN_UI_VERSION = '1.9.5';

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -24,8 +24,8 @@
     "@types/fs-extra": "^9.0.8",
     "@types/node-fetch": "^2.5.8",
     "@types/sharp": "^0.30.4",
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
     "aws-sdk": "^2.856.0",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/common",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",
@@ -49,7 +49,7 @@
     "@nestjs/testing": "7.6.17",
     "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.9.4",
+    "@vendure/common": "^1.9.5",
     "apollo-server-express": "2.24.1",
     "bcrypt": "^5.1.0",
     "body-parser": "^1.19.0",

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -28,13 +28,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.2",
     "@types/semver": "^6.2.2",
-    "@vendure/core": "^1.9.4",
+    "@vendure/core": "^1.9.5",
     "rimraf": "^3.0.2",
     "ts-node": "^10.2.1",
     "typescript": "4.3.5"
   },
   "dependencies": {
-    "@vendure/common": "^1.9.4",
+    "@vendure/common": "^1.9.5",
     "chalk": "^4.1.0",
     "commander": "^7.1.0",
     "cross-spawn": "^7.0.3",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "main": "index.js",
   "license": "MIT",
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^1.9.4",
-    "@vendure/asset-server-plugin": "^1.9.4",
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
-    "@vendure/elasticsearch-plugin": "^1.9.4",
-    "@vendure/email-plugin": "^1.9.4",
+    "@vendure/admin-ui-plugin": "^1.9.5",
+    "@vendure/asset-server-plugin": "^1.9.5",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
+    "@vendure/elasticsearch-plugin": "^1.9.5",
+    "@vendure/email-plugin": "^1.9.5",
     "typescript": "4.3.5"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^1.9.4",
-    "@vendure/ui-devkit": "^1.9.4",
+    "@vendure/testing": "^1.9.5",
+    "@vendure/ui-devkit": "^1.9.5",
     "commander": "^7.1.0",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3",

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -25,8 +25,8 @@
     "fast-deep-equal": "^3.1.3"
   },
   "devDependencies": {
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -35,8 +35,8 @@
     "@types/fs-extra": "^9.0.1",
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.4",
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"
   }

+ 3 - 3
packages/harden-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/harden-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
     "graphql-query-complexity": "^0.12.0"
   },
   "devDependencies": {
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4"
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5"
   }
 }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/job-queue-plugin",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "license": "MIT",
   "main": "package/index.js",
   "types": "package/index.d.ts",
@@ -24,8 +24,8 @@
   "devDependencies": {
     "@google-cloud/pubsub": "^2.8.0",
     "@types/ioredis": "^4.28.10",
-    "@vendure/common": "^1.9.4",
-    "@vendure/core": "^1.9.4",
+    "@vendure/common": "^1.9.5",
+    "@vendure/core": "^1.9.5",
     "bullmq": "^1.86.7",
     "rimraf": "^3.0.2",
     "typescript": "4.3.5"

+ 2 - 0
packages/payments-plugin/e2e/graphql/admin-queries.ts

@@ -83,6 +83,8 @@ export const GET_ORDER_PAYMENTS = gql`
     query order($id: ID!) {
         order(id: $id) {
             id
+            state
+            totalWithTax
             payments {
                 id
                 transactionId

+ 331 - 290
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -156,334 +156,375 @@ describe('Mollie payments', () => {
         expect(customers).toHaveLength(2);
     });
 
-    it('Should prepare an order', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
-            ADD_ITEM_TO_ORDER,
-            {
+    describe('Payment intent creation', () => {
+        it('Should prepare an order', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(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,
+            });
+            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();
         });
-        expect(order.code).toBeDefined();
-    });
 
-    it('Should add a Mollie paymentMethod', async () => {
-        const { createPaymentMethod } = await adminClient.query<
-            CreatePaymentMethod.Mutation,
-            CreatePaymentMethod.Variables
-        >(CREATE_PAYMENT_METHOD, {
-            input: {
-                code: mockData.methodCode,
-                name: 'Mollie payment test',
-                description: 'This is a Mollie test payment method',
-                enabled: true,
-                handler: {
-                    code: molliePaymentHandler.code,
-                    arguments: [
-                        { name: 'redirectUrl', value: mockData.redirectUrl },
-                        { name: 'apiKey', value: mockData.apiKey },
-                        { name: 'autoCapture', value: 'false' },
-                    ],
+        it('Should add a Mollie paymentMethod', async () => {
+            const { createPaymentMethod } = await adminClient.query<
+                CreatePaymentMethod.Mutation,
+                CreatePaymentMethod.Variables
+            >(CREATE_PAYMENT_METHOD, {
+                input: {
+                    code: mockData.methodCode,
+                    name: 'Mollie payment test',
+                    description: 'This is a Mollie test payment method',
+                    enabled: true,
+                    handler: {
+                        code: molliePaymentHandler.code,
+                        arguments: [
+                            { name: 'redirectUrl', value: mockData.redirectUrl },
+                            { name: 'apiKey', value: mockData.apiKey },
+                            { name: 'autoCapture', value: 'false' },
+                        ],
+                    },
                 },
-            },
+            });
+            expect(createPaymentMethod.code).toBe(mockData.methodCode);
         });
-        expect(createPaymentMethod.code).toBe(mockData.methodCode);
-    });
 
-    it('Should fail to create payment intent without shippingmethod', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { createMolliePaymentIntent: result } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-            },
+        it('Should fail to create payment intent without shippingmethod', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            const { createMolliePaymentIntent: result } = await shopClient.query(
+                CREATE_MOLLIE_PAYMENT_INTENT,
+                {
+                    input: {
+                        paymentMethodCode: mockData.methodCode,
+                    },
+                },
+            );
+            expect(result.errorCode).toBe('ORDER_PAYMENT_STATE_ERROR');
         });
-        expect(result.errorCode).toBe('ORDER_PAYMENT_STATE_ERROR');
-    });
 
-    it('Should fail to create payment intent with invalid Mollie method', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        await setShipping(shopClient);
-        const { createMolliePaymentIntent: result } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-                molliePaymentMethodCode: 'invalid',
-            },
+        it('Should fail to create payment intent with invalid Mollie method', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            await setShipping(shopClient);
+            const { createMolliePaymentIntent: result } = await shopClient.query(
+                CREATE_MOLLIE_PAYMENT_INTENT,
+                {
+                    input: {
+                        paymentMethodCode: mockData.methodCode,
+                        molliePaymentMethodCode: 'invalid',
+                    },
+                },
+            );
+            expect(result.errorCode).toBe('INELIGIBLE_PAYMENT_METHOD_ERROR');
         });
-        expect(result.errorCode).toBe('INELIGIBLE_PAYMENT_METHOD_ERROR');
-    });
 
-    it('Should fail to get payment url when items are out of stock', async () => {
-        let { updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
-            input: {
-                id: 'T_5',
-                trackInventory: 'TRUE',
-                outOfStockThreshold: 0,
-                stockOnHand: 1,
-            },
-        });
-        expect(updateProductVariants[0].stockOnHand).toBe(1);
-        const { createMolliePaymentIntent: result } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-            },
+        it('Should fail to get payment url when items are out of stock', async () => {
+            let { updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
+                input: {
+                    id: 'T_5',
+                    trackInventory: 'TRUE',
+                    outOfStockThreshold: 0,
+                    stockOnHand: 1,
+                },
+            });
+            expect(updateProductVariants[0].stockOnHand).toBe(1);
+            const { createMolliePaymentIntent: result } = await shopClient.query(
+                CREATE_MOLLIE_PAYMENT_INTENT,
+                {
+                    input: {
+                        paymentMethodCode: mockData.methodCode,
+                    },
+                },
+            );
+            expect(result.message).toContain('The following variants are out of stock');
+            // Set stock back to not tracking
+            ({ updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
+                input: {
+                    id: 'T_5',
+                    trackInventory: 'FALSE',
+                },
+            }));
+            expect(updateProductVariants[0].trackInventory).toBe('FALSE');
         });
-        expect(result.message).toContain('The following variants are out of stock');
-        // Set stock back to not tracking
-        ({ updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
-            input: {
-                id: 'T_5',
-                trackInventory: 'FALSE',
-            },
-        }));
-        expect(updateProductVariants[0].trackInventory).toBe('FALSE');
-    });
 
-    it('Should get payment url without Mollie method', async () => {
-        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,
-            },
-        });
-        expect(createMolliePaymentIntent).toEqual({
-            url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
+        it('Should get payment url without Mollie method', async () => {
+            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,
+                },
+            });
+            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?.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));
         });
-        expect(mollieRequest?.orderNumber).toEqual(order.code);
-        expect(mollieRequest?.redirectUrl).toEqual(`${mockData.redirectUrl}/${order.code}`);
-        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',
-            },
-        });
-        expect(createMolliePaymentIntent).toEqual({
-            url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
+        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',
+                },
+            });
+            expect(createMolliePaymentIntent).toEqual({
+                url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
+            });
         });
-    });
 
-    it('Should get payment url with deducted amount if a payment is already made', async () => {
-        let mollieRequest: any | undefined;
-        nock('https://api.mollie.com/')
-            .post('/v2/orders', body => {
-                mollieRequest = body;
-                return true;
-            })
-            .reply(200, mockData.mollieOrderResponse);
-        await addManualPayment(server, 1, 10000);
-        await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-            },
+        it('Should get payment url with deducted amount if a payment is already made', async () => {
+            let mollieRequest: any | undefined;
+            nock('https://api.mollie.com/')
+                .post('/v2/orders', body => {
+                    mollieRequest = body;
+                    return true;
+                })
+                .reply(200, mockData.mollieOrderResponse);
+            await addManualPayment(server, 1, 10000);
+            await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
+                input: {
+                    paymentMethodCode: mockData.methodCode,
+                },
+            });
+            expect(mollieRequest.amount?.value).toBe('909.90'); // minus 100,00 from manual payment
+            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));
         });
-        expect(mollieRequest.amount?.value).toBe('909.90'); // minus 100,00 from manual payment
-        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 immediately settle payment for standard payment methods', async () => {
-        nock('https://api.mollie.com/')
-            .get('/v2/orders/ord_mockId')
-            .reply(200, {
-                ...mockData.mollieOrderResponse,
-                orderNumber: order.code,
-                status: OrderStatus.paid,
+        it('Should get available paymentMethods', async () => {
+            nock('https://api.mollie.com/')
+                .get('/v2/methods')
+                .reply(200, mockData.molliePaymentMethodsResponse);
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            const { molliePaymentMethods } = await shopClient.query(GET_MOLLIE_PAYMENT_METHODS, {
+                input: {
+                    paymentMethodCode: mockData.methodCode,
+                },
             });
-        await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
-            method: 'post',
-            body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
-            headers: { 'Content-Type': 'application/json' },
+            const method = molliePaymentMethods[0];
+            expect(method.code).toEqual('ideal');
+            expect(method.minimumAmount).toBeDefined();
+            expect(method.maximumAmount).toBeDefined();
+            expect(method.image).toBeDefined();
         });
-        const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // tslint:disable-next-line:no-non-null-assertion
-        order = orderByCode!;
-        expect(order.state).toBe('PaymentSettled');
     });
 
-    it('Should have Mollie metadata on payment', async () => {
-        const {
-            order: { payments },
-        } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order.id });
-        const metadata = payments[1].metadata;
-        expect(metadata.mode).toBe(mockData.mollieOrderResponse.mode);
-        expect(metadata.method).toBe(mockData.mollieOrderResponse.method);
-        expect(metadata.profileId).toBe(mockData.mollieOrderResponse.profileId);
-        expect(metadata.authorizedAt).toEqual(mockData.mollieOrderResponse.authorizedAt.toISOString());
-        expect(metadata.paidAt).toEqual(mockData.mollieOrderResponse.paidAt.toISOString());
-    });
+    describe('Handle standard payment methods', () => {
+        it('Should transition to ArrangingPayment when partially paid', async () => {
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId')
+                .reply(200, {
+                    ...mockData.mollieOrderResponse,
+                    // Add a payment of 20.00
+                    amount: { value: '20.00', currency: 'EUR' },
+                    orderNumber: order.code,
+                    status: OrderStatus.paid,
+                });
+            await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
+                method: 'post',
+                body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
+                headers: { 'Content-Type': 'application/json' },
+            });
+            // tslint:disable-next-line:no-non-null-assertion
+            const { order: adminOrder } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order!.id });
+            expect(adminOrder.state).toBe('ArrangingPayment');
+        });
 
-    it('Should fail to refund', async () => {
-        nock('https://api.mollie.com/')
-            .get('/v2/orders/ord_mockId?embed=payments')
-            .reply(200, mockData.mollieOrderResponse);
-        nock('https://api.mollie.com/')
-            .post('/v2/payments/tr_mockPayment/refunds')
-            .reply(200, { status: 'failed', resource: 'payment' });
-        const refund = await refundOrderLine(
-            adminClient,
-            order.lines[0].id,
-            1,
+        it('Should place order after paying outstanding amount', async () => {
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId')
+                .reply(200, {
+                    ...mockData.mollieOrderResponse,
+                    // Add a payment of 1089.90
+                    amount: { value: '1089.90', currency: 'EUR' }, // 1109.90 minus the previously paid 20.00
+                    orderNumber: order.code,
+                    status: OrderStatus.paid,
+                });
+            await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
+                method: 'post',
+                body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
+                headers: { 'Content-Type': 'application/json' },
+            });
+            const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
+                GET_ORDER_BY_CODE,
+                {
+                    code: order.code,
+                },
+            );
             // tslint:disable-next-line:no-non-null-assertion
-            order!.payments[1].id,
-            SURCHARGE_AMOUNT,
-        );
-        expect(refund.state).toBe('Failed');
-    });
+            order = orderByCode!;
+            expect(order.state).toBe('PaymentSettled');
+        });
 
-    it('Should successfully refund the Mollie payment', async () => {
-        let mollieRequest;
-        nock('https://api.mollie.com/')
-            .get('/v2/orders/ord_mockId?embed=payments')
-            .reply(200, mockData.mollieOrderResponse);
-        nock('https://api.mollie.com/')
-            .post('/v2/payments/tr_mockPayment/refunds', body => {
-                mollieRequest = body;
-                return true;
-            })
-            .reply(200, { status: 'pending', resource: 'payment' });
-        const refund = await refundOrderLine(
-            adminClient,
-            order.lines[0].id,
-            10,
-            order.payments[1].id,
-            SURCHARGE_AMOUNT,
-        );
-        expect(mollieRequest?.amount.value).toBe('909.90'); // Only refund mollie amount, not the gift card
-        expect(refund.total).toBe(90990);
-        expect(refund.state).toBe('Settled');
-    });
+        it('Should have Mollie metadata on payment', async () => {
+            const {
+                order: { payments },
+            } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order.id });
+            const metadata = payments[1].metadata;
+            expect(metadata.mode).toBe(mockData.mollieOrderResponse.mode);
+            expect(metadata.method).toBe(mockData.mollieOrderResponse.method);
+            expect(metadata.profileId).toBe(mockData.mollieOrderResponse.profileId);
+            expect(metadata.authorizedAt).toEqual(mockData.mollieOrderResponse.authorizedAt.toISOString());
+            expect(metadata.paidAt).toEqual(mockData.mollieOrderResponse.paidAt.toISOString());
+        });
 
-    it('Should get available paymentMethods', async () => {
-        nock('https://api.mollie.com/').get('/v2/methods').reply(200, mockData.molliePaymentMethodsResponse);
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { molliePaymentMethods } = await shopClient.query(GET_MOLLIE_PAYMENT_METHODS, {
-            input: {
-                paymentMethodCode: mockData.methodCode,
-            },
+        it('Should fail to refund', async () => {
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId?embed=payments')
+                .reply(200, mockData.mollieOrderResponse);
+            nock('https://api.mollie.com/')
+                .post('/v2/payments/tr_mockPayment/refunds')
+                .reply(200, { status: 'failed', resource: 'payment' });
+            const refund = await refundOrderLine(
+                adminClient,
+                order.lines[0].id,
+                1,
+                // tslint:disable-next-line:no-non-null-assertion
+                order!.payments[1].id,
+                SURCHARGE_AMOUNT,
+            );
+            expect(refund.state).toBe('Failed');
+        });
+
+        it('Should successfully refund the Mollie payment', async () => {
+            let mollieRequest;
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId?embed=payments')
+                .reply(200, mockData.mollieOrderResponse);
+            nock('https://api.mollie.com/')
+                .post('/v2/payments/tr_mockPayment/refunds', body => {
+                    mollieRequest = body;
+                    return true;
+                })
+                .reply(200, { status: 'pending', resource: 'payment' });
+            const refund = await refundOrderLine(
+                adminClient,
+                order.lines[0].id,
+                10,
+                // tslint:disable-next-line:no-non-null-assertion
+                order.payments!.find(p => p.amount === 108990)!.id,
+                SURCHARGE_AMOUNT,
+            );
+            expect(mollieRequest?.amount.value).toBe('999.90'); // Only refund mollie amount, not the gift card
+            expect(refund.total).toBe(99990);
+            expect(refund.state).toBe('Settled');
         });
-        const method = molliePaymentMethods[0];
-        expect(method.code).toEqual('ideal');
-        expect(method.minimumAmount).toBeDefined();
-        expect(method.maximumAmount).toBeDefined();
-        expect(method.image).toBeDefined();
     });
 
-    it('Should prepare a new order', async () => {
-        await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
-        const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
-            ADD_ITEM_TO_ORDER,
-            {
+    describe('Handle pay-later methods', () => {
+        it('Should prepare a new order', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
                 quantity: 2,
-            },
-        );
-        order = addItemToOrder as TestOrderFragmentFragment;
-        await setShipping(shopClient);
-        expect(order.code).toBeDefined();
-    });
+            });
+            order = addItemToOrder as TestOrderFragmentFragment;
+            await setShipping(shopClient);
+            expect(order.code).toBeDefined();
+        });
 
-    it('Should authorize payment for pay-later payment methods', async () => {
-        nock('https://api.mollie.com/')
-            .get('/v2/orders/ord_mockId')
-            .reply(200, {
-                ...mockData.mollieOrderResponse,
-                orderNumber: order.code,
-                status: OrderStatus.authorized,
+        it('Should authorize payment for pay-later payment methods', async () => {
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId')
+                .reply(200, {
+                    ...mockData.mollieOrderResponse,
+                    amount: { value: '3127.60', currency: 'EUR' },
+                    orderNumber: order.code,
+                    status: OrderStatus.authorized,
+                });
+            await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
+                method: 'post',
+                body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
+                headers: { 'Content-Type': 'application/json' },
             });
-        await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
-            method: 'post',
-            body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
-            headers: { 'Content-Type': 'application/json' },
+            const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
+                GET_ORDER_BY_CODE,
+                {
+                    code: order.code,
+                },
+            );
+            // tslint:disable-next-line:no-non-null-assertion
+            order = orderByCode!;
+            expect(order.state).toBe('PaymentAuthorized');
         });
-        const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // tslint:disable-next-line:no-non-null-assertion
-        order = orderByCode!;
-        expect(order.state).toBe('PaymentAuthorized');
-    });
 
-    it('Should settle payment via settlePayment mutation', async () => {
-        // Mock the getOrder Mollie call
-        nock('https://api.mollie.com/')
-            .get('/v2/orders/ord_mockId')
-            .reply(200, {
-                ...mockData.mollieOrderResponse,
-                orderNumber: order.code,
-                status: OrderStatus.authorized,
+        it('Should settle payment via settlePayment mutation', async () => {
+            // Mock the getOrder Mollie call
+            nock('https://api.mollie.com/')
+                .get('/v2/orders/ord_mockId')
+                .reply(200, {
+                    ...mockData.mollieOrderResponse,
+                    orderNumber: order.code,
+                    status: OrderStatus.authorized,
+                });
+            // Mock the createShipment call
+            let createShipmentBody;
+            nock('https://api.mollie.com/')
+                .post('/v2/orders/ord_mockId/shipments', body => {
+                    createShipmentBody = body;
+                    return true;
+                })
+                .reply(200, { resource: 'shipment', lines: [] });
+            const { settlePayment } = await adminClient.query<
+                SettlePaymentMutation,
+                SettlePaymentMutationVariables
+            >(SETTLE_PAYMENT, {
+                // tslint:disable-next-line:no-non-null-assertion
+                id: order.payments![0].id,
             });
-        // Mock the createShipment call
-        let createShipmentBody;
-        nock('https://api.mollie.com/')
-            .post('/v2/orders/ord_mockId/shipments', body => {
-                createShipmentBody = body;
-                return true;
-            })
-            .reply(200, { resource: 'shipment', lines: [] });
-        const { settlePayment } = await adminClient.query<
-            SettlePaymentMutation,
-            SettlePaymentMutationVariables
-        >(SETTLE_PAYMENT, {
+            const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
+                GET_ORDER_BY_CODE,
+                {
+                    code: order.code,
+                },
+            );
             // tslint:disable-next-line:no-non-null-assertion
-            id: order.payments![0].id,
+            order = orderByCode!;
+            expect(createShipmentBody).toBeDefined();
+            expect(order.state).toBe('PaymentSettled');
         });
-        const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // tslint:disable-next-line:no-non-null-assertion
-        order = orderByCode!;
-        expect(createShipmentBody).toBeDefined();
-        expect(order.state).toBe('PaymentSettled');
     });
 });

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "1.9.4",
+    "version": "1.9.5",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -26,13 +26,16 @@
         "braintree": "3.x",
         "stripe": "8.x"
     },
+    "dependencies": {
+        "currency.js": "2.0.4"
+    },
     "devDependencies": {
         "@mollie/api-client": "^3.6.0",
         "@types/braintree": "^2.22.15",
         "@types/localtunnel": "2.0.1",
-        "@vendure/common": "^1.9.4",
-        "@vendure/core": "^1.9.4",
-        "@vendure/testing": "^1.9.4",
+        "@vendure/common": "^1.9.5",
+        "@vendure/core": "^1.9.5",
+        "@vendure/testing": "^1.9.5",
         "braintree": "^3.0.0",
         "localtunnel": "2.0.1",
         "nock": "^13.1.4",

+ 3 - 3
packages/payments-plugin/src/mollie/mollie.handler.ts

@@ -49,7 +49,7 @@ export const molliePaymentHandler = new PaymentMethodHandler({
     createPayment: async (
         ctx,
         order,
-        amount,
+        _amount, // Don't use this amount, but the amount from the metadata
         args,
         metadata,
     ): Promise<CreatePaymentResult | CreatePaymentErrorResult> => {
@@ -60,9 +60,9 @@ export const molliePaymentHandler = new PaymentMethodHandler({
         if (metadata.status !== 'Authorized' && metadata.status !== 'Settled') {
             throw Error(`Cannot create payment for status ${metadata.status} for order ${order.code}. Only Authorized or Settled are allowed.`);
         }
-        Logger.info(`Payment for order ${order.code} created with state '${metadata.status}'`, loggerCtx);
+        Logger.info(`Payment for order ${order.code} with amount ${metadata.amount} created with state '${metadata.status}'`, loggerCtx);
         return {
-            amount,
+            amount: metadata.amount,
             state: metadata.status,
             transactionId: metadata.orderId, // The plugin now only supports 1 payment per order, so a mollie order equals a payment
             metadata, // Store all given metadata on a payment

+ 8 - 0
packages/payments-plugin/src/mollie/mollie.helpers.ts

@@ -2,6 +2,7 @@ import { CreateParameters } from '@mollie/api-client/dist/types/src/binders/orde
 import { Amount } from '@mollie/api-client/dist/types/src/data/global';
 import { OrderAddress as MollieOrderAddress } from '@mollie/api-client/dist/types/src/data/orders/data';
 import { Customer, Order } from '@vendure/core';
+import currency from 'currency.js';
 
 import { OrderAddress } from './graphql/generated-shop-types';
 
@@ -79,6 +80,13 @@ export function toAmount(value: number, orderCurrency: string): Amount {
     };
 }
 
+/**
+ * Return to number of cents
+ */
+export function amountToCents(amount: Amount): number {
+    return currency(amount.value).intValue;
+}
+
 /**
  * Recalculate tax amount per order line instead of per unit for Mollie.
  * Vendure calculates tax per unit, but Mollie expects the tax to be calculated per order line (the total of the quantities).

+ 2 - 1
packages/payments-plugin/src/mollie/mollie.service.ts

@@ -31,7 +31,7 @@ import {
     MolliePaymentIntentResult,
     MolliePaymentMethod,
 } from './graphql/generated-shop-types';
-import { getLocale, toAmount, toMollieAddress, toMollieOrderLines } from './mollie.helpers';
+import { amountToCents, getLocale, toAmount, toMollieAddress, toMollieOrderLines } from './mollie.helpers';
 import { MolliePluginOptions } from './mollie.plugin';
 
 interface OrderStatusInput {
@@ -219,6 +219,7 @@ export class MollieService {
         const addPaymentToOrderResult = await this.orderService.addPaymentToOrder(ctx, order.id, {
             method: paymentMethodCode,
             metadata: {
+                amount: amountToCents(mollieOrder.amount),
                 status,
                 orderId: mollieOrder.id,
                 mode: mollieOrder.mode,

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^1.9.4",
+    "@vendure/common": "^1.9.5",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "graphql": "15.5.1",
@@ -45,7 +45,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.15",
     "@types/pg": "^7.14.5",
-    "@vendure/core": "^1.9.4",
+    "@vendure/core": "^1.9.5",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "1.9.4",
+  "version": "1.9.5",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -40,8 +40,8 @@
     "@angular/cli": "12.2.16",
     "@angular/compiler": "12.2.16",
     "@angular/compiler-cli": "12.2.16",
-    "@vendure/admin-ui": "^1.9.4",
-    "@vendure/common": "^1.9.4",
+    "@vendure/admin-ui": "^1.9.5",
+    "@vendure/common": "^1.9.5",
     "chalk": "^4.1.0",
     "chokidar": "^3.5.1",
     "fs-extra": "^10.0.0",
@@ -52,7 +52,7 @@
     "@rollup/plugin-node-resolve": "^11.2.0",
     "@types/fs-extra": "^9.0.8",
     "@types/glob": "^7.1.3",
-    "@vendure/core": "^1.9.4",
+    "@vendure/core": "^1.9.5",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",

+ 5 - 0
yarn.lock

@@ -7431,6 +7431,11 @@ cuint@^0.2.2:
   resolved "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
   integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=
 
+currency.js@2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/currency.js/-/currency.js-2.0.4.tgz#a8a4d69be3b2e509bf67a560c78220bc04809cf1"
+  integrity sha512-6/OplJYgJ0RUlli74d93HJ/OsKVBi8lB1+Z6eJYS1YZzBuIp4qKKHpJ7ad+GvTlWmLR/hLJOWTykN5Nm8NJ7+w==
+
 custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"