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,
     Logger,
     LogLevel,
     LogLevel,
     mergeConfig,
     mergeConfig,
-    Order,
     OrderService,
     OrderService,
     RequestContext,
     RequestContext,
 } from '@vendure/core';
 } from '@vendure/core';
@@ -81,8 +80,8 @@ import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
     // Prepare order for payment
     // Prepare order for payment
     await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
     await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
     await shopClient.query<AddItemToOrder.Order, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
     await shopClient.query<AddItemToOrder.Order, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
-        productVariantId: '1',
-        quantity: 2,
+        productVariantId: 'T_5',
+        quantity: 10,
     });
     });
     const ctx = new RequestContext({
     const ctx = new RequestContext({
         apiType: 'admin',
         apiType: 'admin',
@@ -90,7 +89,7 @@ import { CREATE_MOLLIE_PAYMENT_INTENT, setShipping } from './payment-helpers';
         authorizedAsOwnerOnly: false,
         authorizedAsOwnerOnly: false,
         channel: await server.app.get(ChannelService).getDefaultChannel()
         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',
         description: 'Negative test surcharge',
         listPrice: -20000,
         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 () => {
     it('Should prepare an order', async () => {
         await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
         await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
         const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
         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;
         order = addItemToOrder as TestOrderFragmentFragment;
         // Add surcharge
         // Add surcharge
@@ -220,8 +220,9 @@ describe('Mollie payments', () => {
         expect(mollieRequest?.webhookUrl).toEqual(
         expect(mollieRequest?.webhookUrl).toEqual(
             `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
             `${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;
         let totalLineAmount = 0;
         for (const line of mollieRequest.lines) {
         for (const line of mollieRequest.lines) {
             totalLineAmount += Number(line.totalAmount.value);
             totalLineAmount += Number(line.totalAmount.value);
@@ -307,8 +308,8 @@ describe('Mollie payments', () => {
             })
             })
             .reply(200, { status: 'pending', resource: 'payment' });
             .reply(200, { status: 'pending', resource: 'payment' });
         const refund = await refundOne(adminClient, order.lines[0].id, order.payments[0].id);
         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');
         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 => ({
     const lines: CreateParameters['lines'] = order.lines.map(line => ({
         name: line.productVariant.name,
         name: line.productVariant.name,
         quantity: line.quantity,
         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),
         totalAmount: toAmount(line.proratedLinePriceWithTax, order.currencyCode),
         vatRate: String(line.taxRate),
         vatRate: String(line.taxRate),
-        vatAmount: toAmount(line.lineTax, order.currencyCode),
+        vatAmount: toAmount(
+            calculateLineTaxAmount(line.taxRate, line.proratedLinePriceWithTax),
+            order.currencyCode
+        ),
     }));
     }));
     // Add shippingLines
     // Add shippingLines
     lines.push(...order.shippingLines.map(line => ({
     lines.push(...order.shippingLines.map(line => ({
         name: line.shippingMethod?.name || 'Shipping',
         name: line.shippingMethod?.name || 'Shipping',
         quantity: 1,
         quantity: 1,
-        unitPrice: toAmount(line.priceWithTax, order.currencyCode),
+        unitPrice: toAmount(line.discountedPriceWithTax, order.currencyCode),
         totalAmount: toAmount(line.discountedPriceWithTax, order.currencyCode),
         totalAmount: toAmount(line.discountedPriceWithTax, order.currencyCode),
         vatRate: String(line.taxRate),
         vatRate: String(line.taxRate),
         vatAmount: toAmount(line.discountedPriceWithTax - line.discountedPrice, order.currencyCode),
         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 => ({
     lines.push(...order.surcharges.map(surcharge => ({
         name: surcharge.description,
         name: surcharge.description,
         quantity: 1,
         quantity: 1,
-        unitPrice: toAmount(surcharge.price, order.currencyCode),
+        unitPrice: toAmount(surcharge.priceWithTax, order.currencyCode),
         totalAmount: toAmount(surcharge.priceWithTax, order.currencyCode),
         totalAmount: toAmount(surcharge.priceWithTax, order.currencyCode),
         vatRate: String(surcharge.taxRate),
         vatRate: String(surcharge.taxRate),
         vatAmount: toAmount(surcharge.priceWithTax - surcharge.price, order.currencyCode),
         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.
  * 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
  * If both lookups fail, resolve to en_US to prevent payment failure