Browse Source

fix(payments-plugin): Calculate tax per order line instead of per unit for Mollie (#1958)

Fixes #1939
Martijn 3 years ago
parent
commit
16b17b61fb

+ 6 - 5
packages/payments-plugin/e2e/fixtures/e2e-products-minimal.csv

@@ -1,5 +1,6 @@
-name   , slug   , description                                                                                                                                                                                                                                                                  , assets                           , facets                                  , optionGroups       , optionValues    , sku      , price   , taxCategory , stockOnHand , trackInventory , variantAssets , variantFacets
-Laptop , laptop , "Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz." ,                                  , category:electronics|category:computers , "screen size|RAM"  , "13 inch|8GB"   , L2201308 , 1299.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch|8GB"   , L2201508 , 1399.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "13 inch|16GB"  , L2201316 , 2199.00 , standard    , 100         , false          ,               ,
-       ,        ,                                                                                                                                                                                                                                                                              ,                                  ,                                         ,                    , "15 inch|16GB"  , L2201516 , 2299.00 , standard    , 100         , false          ,               ,
+name            ,slug    ,description                                                                                                                                                                                                                                                                 ,assets,facets                                 ,optionGroups     ,optionValues  ,sku        ,price  ,taxCategory,stockOnHand,trackInventory,variantAssets,variantFacets
+Laptop          ,laptop  ,"Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz.",      ,category:electronics|category:computers,"screen size|RAM","13 inch|8GB" ,L2201308   ,1299.00,standard   ,100        ,false         ,             ,
+                ,        ,                                                                                                                                                                                                                                                                            ,      ,                                       ,                 ,"15 inch|8GB" ,L2201508   ,1399.00,standard   ,100        ,false         ,             ,
+                ,        ,                                                                                                                                                                                                                                                                            ,      ,                                       ,                 ,"13 inch|16GB",L2201316   ,2199.00,standard   ,100        ,false         ,             ,
+                ,        ,                                                                                                                                                                                                                                                                            ,      ,                                       ,                 ,"15 inch|16GB",L2201516   ,2299.00,standard   ,100        ,false         ,             ,
+Pinelab stickers,stickers,"Very nice but crazy expensive stickers for testing Mollie VAT rounding errors"                                                                                                                                                                                             ,      ,category:computers                     ,                 ,              ,pl-stickers,99.99  ,standard   ,100        ,false         ,             ,

+ 3 - 4
packages/payments-plugin/e2e/mollie-dev-server.ts

@@ -5,7 +5,6 @@ import {
     Logger,
     LogLevel,
     mergeConfig,
-    Order,
     OrderService,
     RequestContext,
 } from '@vendure/core';
@@ -81,8 +80,8 @@ import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
     // Prepare order for payment
     await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
     await shopClient.query<AddItemToOrder.Order, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
-        productVariantId: '1',
-        quantity: 2,
+        productVariantId: 'T_5',
+        quantity: 10,
     });
     const ctx = new RequestContext({
         apiType: 'admin',
@@ -90,7 +89,7 @@ import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
         authorizedAsOwnerOnly: false,
         channel: await server.app.get(ChannelService).getDefaultChannel()
     });
-    await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
+   await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
         description: 'Negative test surcharge',
         listPrice: -20000,
     });

+ 7 - 6
packages/payments-plugin/e2e/mollie-payment.e2e-spec.ts

@@ -138,8 +138,8 @@ describe('Mollie payments', () => {
     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_1',
-            quantity: 2,
+            productVariantId: 'T_5',
+            quantity: 10,
         });
         order = addItemToOrder as TestOrderFragmentFragment;
         // Add surcharge
@@ -220,8 +220,9 @@ describe('Mollie payments', () => {
         expect(mollieRequest?.webhookUrl).toEqual(
             `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
         );
-        expect(mollieRequest?.amount?.value).toBe('2927.60');
-        expect(mollieRequest?.amount?.currency).toBeDefined();
+        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);
@@ -307,8 +308,8 @@ describe('Mollie payments', () => {
             })
             .reply(200, { status: 'pending', resource: 'payment' });
         const refund = await refundOne(adminClient, order.lines[0].id, order.payments[0].id);
-        expect(mollieRequest?.amount.value).toBe('1558.80');
-        expect(refund.total).toBe(155880);
+        expect(mollieRequest?.amount.value).toBe('119.99');
+        expect(refund.total).toBe(11999);
         expect(refund.state).toBe('Settled');
     });
 

+ 18 - 4
packages/payments-plugin/src/mollie/mollie.helpers.ts

@@ -31,16 +31,19 @@ export function toMollieOrderLines(order: Order): CreateParameters['lines'] {
     const lines: CreateParameters['lines'] = order.lines.map(line => ({
         name: line.productVariant.name,
         quantity: line.quantity,
-        unitPrice: toAmount(line.proratedUnitPriceWithTax, order.currencyCode), // totalAmount has to match unitPrice * quantity
+        unitPrice: toAmount(line.proratedLinePriceWithTax / line.quantity, order.currencyCode), // totalAmount has to match unitPrice * quantity
         totalAmount: toAmount(line.proratedLinePriceWithTax, order.currencyCode),
         vatRate: String(line.taxRate),
-        vatAmount: toAmount(line.lineTax, order.currencyCode),
+        vatAmount: toAmount(
+            calculateLineTaxAmount(line.taxRate, line.proratedLinePriceWithTax),
+            order.currencyCode
+        ),
     }));
     // Add shippingLines
     lines.push(...order.shippingLines.map(line => ({
         name: line.shippingMethod?.name || 'Shipping',
         quantity: 1,
-        unitPrice: toAmount(line.priceWithTax, order.currencyCode),
+        unitPrice: toAmount(line.discountedPriceWithTax, order.currencyCode),
         totalAmount: toAmount(line.discountedPriceWithTax, order.currencyCode),
         vatRate: String(line.taxRate),
         vatAmount: toAmount(line.discountedPriceWithTax - line.discountedPrice, order.currencyCode),
@@ -49,7 +52,7 @@ export function toMollieOrderLines(order: Order): CreateParameters['lines'] {
     lines.push(...order.surcharges.map(surcharge => ({
         name: surcharge.description,
         quantity: 1,
-        unitPrice: toAmount(surcharge.price, order.currencyCode),
+        unitPrice: toAmount(surcharge.priceWithTax, order.currencyCode),
         totalAmount: toAmount(surcharge.priceWithTax, order.currencyCode),
         vatRate: String(surcharge.taxRate),
         vatAmount: toAmount(surcharge.priceWithTax - surcharge.price, order.currencyCode),
@@ -67,6 +70,17 @@ export function toAmount(value: number, orderCurrency: string): Amount {
     };
 }
 
+/**
+ * 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).
+ * See https://github.com/vendure-ecommerce/vendure/issues/1939#issuecomment-1362962133 for more information on the rounding issue.
+ */
+export function calculateLineTaxAmount(taxRate: number, orderLinePriceWithTax: number): number {
+    const taxMultiplier = taxRate / 100;
+    return orderLinePriceWithTax * (taxMultiplier / (1+taxMultiplier)); // I.E. €99,99 * (0,2 ÷ 1,2) with a 20% taxrate
+
+}
+
 /**
  * Lookup one of Mollies allowed locales based on an orders countrycode or channel default.
  * If both lookups fail, resolve to en_US to prevent payment failure