Kaynağa Gözat

Merge branch 'minor' into major

Michael Bromley 2 yıl önce
ebeveyn
işleme
f81c5e875c

+ 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>
 
 

+ 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

+ 338 - 297
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -161,339 +161,380 @@ 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<
-            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,
+    describe('Payment intent creation', () => {
+        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();
         });
-        expect(order.code).toBeDefined();
-    });
 
-    it('Should add a Mollie paymentMethod', async () => {
-        const { createPaymentMethod } = await adminClient.query<
-            CreatePaymentMethodMutation,
-            CreatePaymentMethodMutationVariables
-        >(CREATE_PAYMENT_METHOD, {
-            input: {
-                code: mockData.methodCode,
-                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<
+                CreatePaymentMethodMutation,
+                CreatePaymentMethodMutationVariables
+            >(CREATE_PAYMENT_METHOD, {
+                input: {
+                    code: mockData.methodCode,
+                    enabled: true,
+                    handler: {
+                        code: molliePaymentHandler.code,
+                        arguments: [
+                            { name: 'redirectUrl', value: mockData.redirectUrl },
+                            { 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',
+                        },
                     ],
                 },
-                translations: [
-                    {
-                        languageCode: LanguageCode.en,
-                        name: 'Mollie payment test',
-                        description: 'This is a Mollie test payment method',
-                    },
-                ],
-            },
+            });
+            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<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // eslint-disable-next-line @typescript-eslint/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,
+                },
+            );
             // eslint-disable-next-line @typescript-eslint/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,
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                order!.payments[1].id,
+                SURCHARGE_AMOUNT,
+            );
+            expect(refund.state).toBe('Failed');
         });
-        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<
-            AddItemToOrderMutation,
-            AddItemToOrderMutationVariables
-        >(ADD_ITEM_TO_ORDER, {
-            productVariantId: 'T_1',
-            quantity: 2,
+        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');
         });
-        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,
+    describe('Handle pay-later methods', () => {
+        it('Should prepare a new order', async () => {
+            await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrderMutation,
+                AddItemToOrderMutationVariables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_1',
+                quantity: 2,
             });
-        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' },
+            order = addItemToOrder as TestOrderFragmentFragment;
+            await setShipping(shopClient);
+            expect(order.code).toBeDefined();
         });
-        const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // eslint-disable-next-line @typescript-eslint/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 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' },
             });
-        // 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<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
+                GET_ORDER_BY_CODE,
+                {
+                    code: order.code,
+                },
+            );
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-            id: order.payments![0].id,
+            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,
+                });
+            // 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, {
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                id: order.payments![0].id,
+            });
+            const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
+                GET_ORDER_BY_CODE,
+                {
+                    code: order.code,
+                },
+            );
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            order = orderByCode!;
+            expect(createShipmentBody).toBeDefined();
+            expect(order.state).toBe('PaymentSettled');
         });
-        const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
-            GET_ORDER_BY_CODE,
-            {
-                code: order.code,
-            },
-        );
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        order = orderByCode!;
-        expect(createShipmentBody).toBeDefined();
-        expect(order.state).toBe('PaymentSettled');
     });
 });

+ 3 - 0
packages/payments-plugin/package.json

@@ -27,6 +27,9 @@
         "braintree": "3.x",
         "stripe": "8.x"
     },
+    "dependencies": {
+        "currency.js": "2.0.4"
+    },
     "peerDependenciesMeta": {
         "@mollie/api-client": {
             "optional": true

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

@@ -55,7 +55,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> => {
@@ -70,12 +70,9 @@ export const molliePaymentHandler = new PaymentMethodHandler({
                 }. Only Authorized or Settled are allowed.`,
             );
         }
-        Logger.info(
-            `Payment for order ${order.code} created with state '${metadata.status as string}'`,
-            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';
 
@@ -81,6 +82,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 {
@@ -245,6 +245,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,

+ 5 - 0
yarn.lock

@@ -7244,6 +7244,11 @@ cuint@^0.2.2:
   resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
   integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==
 
+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.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"