فهرست منبع

feat(core): Modify ShippingCalculator API to enable correct tax handling

Relates to #580, Relates to #573.

BREAKING CHANGE: The return object of the ShippingCalculator class has changed:
    ```ts
    // before
    return {
      price: 500,
      priceWithTax: 600,
    };

    // after
    return {
      price: 500,
      taxRate: 20,
      priceIncludesTax: false,
    };
    ```
    This change will require you to update any custom ShippingCalculator implementations, and also
    to update any ShippingMethods by removing and re-selecting the ShippingCalculator.
Michael Bromley 5 سال پیش
والد
کامیت
1ab1c811ec

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts

@@ -125,6 +125,7 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
             this.subscription.unsubscribe();
         }
         this.form = new FormGroup({});
+        (this.form as any).__id = Math.random().toString(36).substr(10);
 
         if (this.operation.args) {
             for (const arg of this.operationDefinition?.args || []) {

+ 4 - 2
packages/common/src/shared-types.ts

@@ -1,6 +1,6 @@
 // tslint:disable:no-shadowed-variable
 // prettier-ignore
-import { LanguageCode } from './generated-types';
+import { LanguageCode, LocalizedString } from './generated-types';
 
 /**
  * A recursive implementation of the Partial<T> type.
@@ -136,7 +136,9 @@ export type DefaultFormComponentId =
 type DefaultFormConfigHash = {
     'date-form-input': { min?: string; max?: string; yearRange?: number };
     'number-form-input': { min?: number; max?: number; step?: number; prefix?: string; suffix?: string };
-    'select-form-input': { options?: Array<{ value: string; label?: string }> };
+    'select-form-input': {
+        options?: Array<{ value: string; label?: Array<Omit<LocalizedString, '__typename'>> }>;
+    };
     'boolean-form-input': {};
     'currency-form-input': {};
     'facet-value-form-input': {};

+ 87 - 5
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -4,8 +4,11 @@ import { pick } from '@vendure/common/lib/pick';
 import {
     containsProducts,
     customerGroup,
+    defaultShippingCalculator,
+    defaultShippingEligibilityChecker,
     discountOnItemWithFacets,
     hasFacetValues,
+    manualFulfillmentHandler,
     minimumOrderAmount,
     orderPercentageDiscount,
     productsPercentageDiscount,
@@ -32,6 +35,7 @@ import {
     CreateCustomerGroup,
     CreatePromotion,
     CreatePromotionInput,
+    CreateShippingMethod,
     CurrencyCode,
     GetFacetList,
     GetProductsWithVariantPrices,
@@ -60,6 +64,7 @@ import {
     CREATE_CHANNEL,
     CREATE_CUSTOMER_GROUP,
     CREATE_PROMOTION,
+    CREATE_SHIPPING_METHOD,
     GET_FACET_LIST,
     GET_PRODUCTS_WITH_VARIANT_PRICES,
     REMOVE_CUSTOMERS_FROM_GROUP,
@@ -911,6 +916,43 @@ describe('Promotions applied to Orders', () => {
             const couponCode = 'FREE_SHIPPING';
             let promotion: PromotionFragment;
 
+            // The test shipping method needs to be created in each Channel, since ShippingMethods
+            // are ChannelAware
+            async function createTestShippingMethod(channelToken: string) {
+                adminClient.setChannelToken(channelToken);
+                const result = await adminClient.query<
+                    CreateShippingMethod.Mutation,
+                    CreateShippingMethod.Variables
+                >(CREATE_SHIPPING_METHOD, {
+                    input: {
+                        code: 'test-method',
+                        fulfillmentHandler: manualFulfillmentHandler.code,
+                        checker: {
+                            code: defaultShippingEligibilityChecker.code,
+                            arguments: [
+                                {
+                                    name: 'orderMinimum',
+                                    value: '0',
+                                },
+                            ],
+                        },
+                        calculator: {
+                            code: defaultShippingCalculator.code,
+                            arguments: [
+                                { name: 'rate', value: '345' },
+                                { name: 'includesTax', value: 'auto' },
+                                { name: 'taxRate', value: '20' },
+                            ],
+                        },
+                        translations: [
+                            { languageCode: LanguageCode.en, name: 'test method', description: '' },
+                        ],
+                    },
+                });
+                adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
+                return result.createShippingMethod;
+            }
+
             beforeAll(async () => {
                 promotion = await createPromotion({
                     enabled: true,
@@ -939,18 +981,19 @@ describe('Promotions applied to Orders', () => {
                     productVariantId: getVariantBySlug('item-5000').id,
                     quantity: 1,
                 });
+                const method = await createTestShippingMethod(E2E_DEFAULT_CHANNEL_TOKEN);
                 const { setOrderShippingMethod } = await shopClient.query<
                     SetShippingMethod.Mutation,
                     SetShippingMethod.Variables
                 >(SET_SHIPPING_METHOD, {
-                    id: 'T_1',
+                    id: method.id,
                 });
                 orderResultGuard.assertSuccess(setOrderShippingMethod);
                 expect(setOrderShippingMethod.discounts).toEqual([]);
-                expect(setOrderShippingMethod.shipping).toBe(500);
-                expect(setOrderShippingMethod.shippingWithTax).toBe(500);
-                expect(setOrderShippingMethod.total).toBe(5500);
-                expect(setOrderShippingMethod.totalWithTax).toBe(6500);
+                expect(setOrderShippingMethod.shipping).toBe(345);
+                expect(setOrderShippingMethod.shippingWithTax).toBe(414);
+                expect(setOrderShippingMethod.total).toBe(5345);
+                expect(setOrderShippingMethod.totalWithTax).toBe(6414);
 
                 const { applyCouponCode } = await shopClient.query<
                     ApplyCouponCode.Mutation,
@@ -967,6 +1010,45 @@ describe('Promotions applied to Orders', () => {
                 expect(applyCouponCode.total).toBe(5000);
                 expect(applyCouponCode.totalWithTax).toBe(6000);
             });
+
+            it('prices include tax', async () => {
+                shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
+                const { addItemToOrder } = await shopClient.query<
+                    AddItemToOrder.Mutation,
+                    AddItemToOrder.Variables
+                >(ADD_ITEM_TO_ORDER, {
+                    productVariantId: getVariantBySlug('item-5000').id,
+                    quantity: 1,
+                });
+                const method = await createTestShippingMethod(TAX_INCLUDED_CHANNEL_TOKEN);
+                const { setOrderShippingMethod } = await shopClient.query<
+                    SetShippingMethod.Mutation,
+                    SetShippingMethod.Variables
+                >(SET_SHIPPING_METHOD, {
+                    id: method.id,
+                });
+                orderResultGuard.assertSuccess(setOrderShippingMethod);
+                expect(setOrderShippingMethod.discounts).toEqual([]);
+                expect(setOrderShippingMethod.shipping).toBe(287);
+                expect(setOrderShippingMethod.shippingWithTax).toBe(345);
+                expect(setOrderShippingMethod.total).toBe(4454);
+                expect(setOrderShippingMethod.totalWithTax).toBe(5345);
+
+                const { applyCouponCode } = await shopClient.query<
+                    ApplyCouponCode.Mutation,
+                    ApplyCouponCode.Variables
+                >(APPLY_COUPON_CODE, {
+                    couponCode,
+                });
+                orderResultGuard.assertSuccess(applyCouponCode);
+
+                expect(applyCouponCode.discounts.length).toBe(1);
+                expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
+                expect(applyCouponCode.shipping).toBe(0);
+                expect(applyCouponCode.shippingWithTax).toBe(0);
+                expect(applyCouponCode.total).toBe(4167);
+                expect(applyCouponCode.totalWithTax).toBe(5000);
+            });
         });
 
         describe('multiple promotions simultaneously', () => {

+ 2 - 1
packages/core/e2e/shipping-method-eligibility.e2e-spec.ts

@@ -82,7 +82,8 @@ const calculator = new ShippingCalculator({
     calculate: ctx => {
         return {
             price: 10,
-            priceWithTax: 12,
+            priceIncludesTax: false,
+            taxRate: 20,
         };
     },
 });

+ 27 - 1
packages/core/e2e/shipping-method.e2e-spec.ts

@@ -40,7 +40,8 @@ const calculatorWithMetadata = new ShippingCalculator({
     calculate: () => {
         return {
             price: 100,
-            priceWithTax: 100,
+            priceIncludesTax: true,
+            taxRate: 0,
             metadata: TEST_METADATA,
         };
     },
@@ -107,6 +108,31 @@ describe('ShippingMethod resolver', () => {
                         name: 'rate',
                         type: 'int',
                     },
+                    {
+                        label: 'Price includes tax',
+                        name: 'includesTax',
+                        type: 'string',
+                        description: null,
+                        ui: {
+                            component: 'select-form-input',
+                            options: [
+                                {
+                                    label: [{ languageCode: LanguageCode.en, value: 'Includes tax' }],
+                                    value: 'include',
+                                },
+                                {
+                                    label: [{ languageCode: LanguageCode.en, value: 'Excludes tax' }],
+                                    value: 'exclude',
+                                },
+                                {
+                                    label: [
+                                        { languageCode: LanguageCode.en, value: 'Auto (based on Channel)' },
+                                    ],
+                                    value: 'auto',
+                                },
+                            ],
+                        },
+                    },
                     {
                         ui: {
                             component: 'number-form-input',

+ 1 - 1
packages/core/src/config/promotion/actions/free-shipping-action.ts

@@ -6,7 +6,7 @@ export const freeShipping = new PromotionShippingAction({
     code: 'free_shipping',
     args: {},
     execute(ctx, shippingLine, order, args) {
-        return -shippingLine.priceWithTax;
+        return ctx.channel.pricesIncludeTax ? -shippingLine.priceWithTax : -shippingLine.price;
     },
     description: [{ languageCode: LanguageCode.en, value: 'Free shipping' }],
 });

+ 45 - 1
packages/core/src/config/shipping-method/default-shipping-calculator.ts

@@ -1,7 +1,15 @@
 import { LanguageCode } from '@vendure/common/lib/generated-types';
 
+import { RequestContext } from '../../api/common/request-context';
+
 import { ShippingCalculator } from './shipping-calculator';
 
+enum TaxSetting {
+    include = 'include',
+    exclude = 'exclude',
+    auto = 'auto',
+}
+
 export const defaultShippingCalculator = new ShippingCalculator({
     code: 'default-shipping-calculator',
     description: [{ languageCode: LanguageCode.en, value: 'Default Flat-Rate Shipping Calculator' }],
@@ -11,6 +19,27 @@ export const defaultShippingCalculator = new ShippingCalculator({
             ui: { component: 'currency-form-input' },
             label: [{ languageCode: LanguageCode.en, value: 'Shipping price' }],
         },
+        includesTax: {
+            type: 'string',
+            ui: {
+                component: 'select-form-input',
+                options: [
+                    {
+                        label: [{ languageCode: LanguageCode.en, value: 'Includes tax' }],
+                        value: TaxSetting.include,
+                    },
+                    {
+                        label: [{ languageCode: LanguageCode.en, value: 'Excludes tax' }],
+                        value: TaxSetting.exclude,
+                    },
+                    {
+                        label: [{ languageCode: LanguageCode.en, value: 'Auto (based on Channel)' }],
+                        value: TaxSetting.auto,
+                    },
+                ],
+            },
+            label: [{ languageCode: LanguageCode.en, value: 'Price includes tax' }],
+        },
         taxRate: {
             type: 'int',
             ui: { component: 'number-form-input', suffix: '%' },
@@ -18,6 +47,21 @@ export const defaultShippingCalculator = new ShippingCalculator({
         },
     },
     calculate: (ctx, order, args) => {
-        return { price: args.rate, priceWithTax: args.rate * ((100 + args.taxRate) / 100) };
+        return {
+            price: args.rate,
+            taxRate: args.taxRate,
+            priceIncludesTax: getPriceIncludesTax(ctx, args.includesTax as any),
+        };
     },
 });
+
+function getPriceIncludesTax(ctx: RequestContext, setting: TaxSetting): boolean {
+    switch (setting) {
+        case TaxSetting.auto:
+            return ctx.channel.pricesIncludeTax;
+        case TaxSetting.exclude:
+            return false;
+        case TaxSetting.include:
+            return true;
+    }
+}

+ 7 - 2
packages/core/src/config/shipping-method/shipping-calculator.ts

@@ -71,9 +71,14 @@ export interface ShippingCalculationResult {
     price: number;
     /**
      * @description
-     * The shipping price including taxes.
+     * Whether or not the given price already includes taxes.
      */
-    priceWithTax: number;
+    priceIncludesTax: boolean;
+    /**
+     * @description
+     * The tax rate applied to the shipping price.
+     */
+    taxRate: number;
     /**
      * @description
      * Arbitrary metadata may be returned from the calculation function. This can be used

+ 6 - 6
packages/core/src/entity/order-item/order-item.entity.ts

@@ -74,6 +74,11 @@ export class OrderItem extends VendureEntity {
         return this.listPriceIncludesTax ? netPriceOf(this.listPrice, this.taxRate) : this.listPrice;
     }
 
+    @Calculated()
+    get unitPriceWithTax(): number {
+        return this.listPriceIncludesTax ? this.listPrice : grossPriceOf(this.listPrice, this.taxRate);
+    }
+
     /**
      * @description
      * The total applicable tax rate, which is the sum of all taxLines on this
@@ -81,12 +86,7 @@ export class OrderItem extends VendureEntity {
      */
     @Calculated()
     get taxRate(): number {
-        return summate(this.taxLines || [], 'taxRate');
-    }
-
-    @Calculated()
-    get unitPriceWithTax(): number {
-        return this.listPriceIncludesTax ? this.listPrice : grossPriceOf(this.listPrice, this.taxRate);
+        return summate(this.taxLines, 'taxRate');
     }
 
     @Calculated()

+ 27 - 7
packages/core/src/entity/shipping-line/shipping-line.entity.ts

@@ -1,4 +1,4 @@
-import { Adjustment, AdjustmentType } from '@vendure/common/lib/generated-types';
+import { Adjustment, AdjustmentType, TaxLine } from '@vendure/common/lib/generated-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { summate } from '@vendure/common/lib/shared-utils';
 import { Column, Entity, ManyToOne } from 'typeorm';
@@ -26,23 +26,43 @@ export class ShippingLine extends VendureEntity {
     order: Order;
 
     @Column()
-    price: number;
+    listPrice: number;
 
     @Column()
-    priceWithTax: number;
+    listPriceIncludesTax: boolean;
+
+    @Column('simple-json')
+    adjustments: Adjustment[];
+
+    @Column('simple-json')
+    taxLines: TaxLine[];
+
+    @Calculated()
+    get price(): number {
+        return this.listPriceIncludesTax ? netPriceOf(this.listPrice, this.taxRate) : this.listPrice;
+    }
+
+    @Calculated()
+    get priceWithTax(): number {
+        return this.listPriceIncludesTax ? this.listPrice : grossPriceOf(this.listPrice, this.taxRate);
+    }
 
     @Calculated()
     get discountedPrice(): number {
-        return this.price + this.getAdjustmentsTotal();
+        const result = this.listPrice + this.getAdjustmentsTotal();
+        return this.listPriceIncludesTax ? netPriceOf(result, this.taxRate) : result;
     }
 
     @Calculated()
     get discountedPriceWithTax(): number {
-        return this.priceWithTax + this.getAdjustmentsTotal();
+        const result = this.listPrice + this.getAdjustmentsTotal();
+        return this.listPriceIncludesTax ? result : grossPriceOf(result, this.taxRate);
     }
 
-    @Column('simple-json')
-    adjustments: Adjustment[];
+    @Calculated()
+    get taxRate(): number {
+        return summate(this.taxLines, 'taxRate');
+    }
 
     @Calculated()
     get discounts(): Adjustment[] {

+ 3 - 2
packages/core/src/entity/shipping-method/shipping-method.entity.ts

@@ -75,10 +75,11 @@ export class ShippingMethod
         if (calculator) {
             const response = await calculator.calculate(ctx, order, this.calculator.args);
             if (response) {
-                const { price, priceWithTax, metadata } = response;
+                const { price, priceIncludesTax, taxRate, metadata } = response;
                 return {
                     price: Math.round(price),
-                    priceWithTax: Math.round(priceWithTax),
+                    priceIncludesTax,
+                    taxRate,
                     metadata,
                 };
             }

+ 92 - 21
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -37,18 +37,20 @@ import { OrderCalculator } from './order-calculator';
 describe('OrderCalculator', () => {
     let orderCalculator: OrderCalculator;
 
-    const mockShippingMethod = {
-        price: 500,
-        priceWithTax: 600,
-        id: 'T_1',
-        test: () => true,
-        apply() {
-            return {
-                price: this.price,
-                priceWithTax: this.priceWithTax,
-            };
-        },
-    };
+    const mockShippingMethodId = 'T_1';
+    function createMockShippingMethod(ctx: RequestContext) {
+        return {
+            id: mockShippingMethodId,
+            test: () => true,
+            apply() {
+                return {
+                    price: 500,
+                    priceIncludesTax: ctx.channel.pricesIncludeTax,
+                    taxRate: 20,
+                };
+            },
+        };
+    }
 
     beforeEach(async () => {
         const module = await Test.createTestingModule({
@@ -57,7 +59,10 @@ describe('OrderCalculator', () => {
                 TaxCalculator,
                 TaxRateService,
                 { provide: ShippingCalculator, useValue: { getEligibleShippingMethods: () => [] } },
-                { provide: ShippingMethodService, useValue: { findOne: () => mockShippingMethod } },
+                {
+                    provide: ShippingMethodService,
+                    useValue: { findOne: (ctx: RequestContext) => createMockShippingMethod(ctx) },
+                },
                 { provide: TransactionalConnection, useClass: MockConnection },
                 { provide: ListQueryBuilder, useValue: {} },
                 { provide: ConfigService, useClass: MockConfigService },
@@ -142,16 +147,43 @@ describe('OrderCalculator', () => {
             });
             order.shippingLines = [
                 new ShippingLine({
-                    shippingMethodId: mockShippingMethod.id,
+                    shippingMethodId: mockShippingMethodId,
                 }),
             ];
             await orderCalculator.applyPriceAdjustments(ctx, order, []);
 
             expect(order.subTotal).toBe(100);
-            expect(order.shipping).toBe(mockShippingMethod.price);
-            expect(order.shippingWithTax).toBe(mockShippingMethod.priceWithTax);
-            expect(order.total).toBe(order.subTotal + mockShippingMethod.price);
-            expect(order.totalWithTax).toBe(order.subTotalWithTax + mockShippingMethod.priceWithTax);
+            expect(order.shipping).toBe(500);
+            expect(order.shippingWithTax).toBe(600);
+            expect(order.total).toBe(order.subTotal + 500);
+            expect(order.totalWithTax).toBe(order.subTotalWithTax + 600);
+            assertOrderTotalsAddUp(order);
+        });
+
+        it('prices include tax', async () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const order = createOrder({
+                ctx,
+                lines: [
+                    {
+                        listPrice: 100,
+                        taxCategory: taxCategoryStandard,
+                        quantity: 1,
+                    },
+                ],
+            });
+            order.shippingLines = [
+                new ShippingLine({
+                    shippingMethodId: mockShippingMethodId,
+                }),
+            ];
+            await orderCalculator.applyPriceAdjustments(ctx, order, []);
+
+            expect(order.subTotal).toBe(83);
+            expect(order.shipping).toBe(417);
+            expect(order.shippingWithTax).toBe(500);
+            expect(order.total).toBe(order.subTotal + 417);
+            expect(order.totalWithTax).toBe(order.subTotalWithTax + 500);
             assertOrderTotalsAddUp(order);
         });
     });
@@ -231,7 +263,7 @@ describe('OrderCalculator', () => {
             description: [{ languageCode: LanguageCode.en, value: 'Free shipping' }],
             args: {},
             execute(ctx, shippingLine, order, args) {
-                return -shippingLine.price;
+                return ctx.channel.pricesIncludeTax ? -shippingLine.priceWithTax : -shippingLine.price;
             },
         });
 
@@ -625,7 +657,7 @@ describe('OrderCalculator', () => {
                     });
                     order.shippingLines = [
                         new ShippingLine({
-                            shippingMethodId: mockShippingMethod.id,
+                            shippingMethodId: mockShippingMethodId,
                             adjustments: [],
                         }),
                     ];
@@ -633,7 +665,7 @@ describe('OrderCalculator', () => {
 
                     expect(order.subTotal).toBe(100);
                     expect(order.discounts.length).toBe(0);
-                    expect(order.total).toBe(order.subTotal + mockShippingMethod.price);
+                    expect(order.total).toBe(order.subTotal + 500);
                     assertOrderTotalsAddUp(order);
 
                     order.couponCodes = [couponCode];
@@ -642,6 +674,45 @@ describe('OrderCalculator', () => {
                     expect(order.subTotal).toBe(100);
                     expect(order.discounts.length).toBe(1);
                     expect(order.discounts[0].description).toBe('Free shipping');
+                    expect(order.shipping).toBe(0);
+                    expect(order.shippingWithTax).toBe(0);
+                    expect(order.total).toBe(order.subTotal);
+                    assertOrderTotalsAddUp(order);
+                });
+
+                it('prices include tax', async () => {
+                    const ctx = createRequestContext({ pricesIncludeTax: true });
+                    const order = createOrder({
+                        ctx,
+                        lines: [
+                            {
+                                listPrice: 100,
+                                taxCategory: taxCategoryStandard,
+                                quantity: 1,
+                            },
+                        ],
+                    });
+                    order.shippingLines = [
+                        new ShippingLine({
+                            shippingMethodId: mockShippingMethodId,
+                            adjustments: [],
+                        }),
+                    ];
+                    await orderCalculator.applyPriceAdjustments(ctx, order, [promotion]);
+
+                    expect(order.subTotal).toBe(83);
+                    expect(order.discounts.length).toBe(0);
+                    expect(order.total).toBe(order.subTotal + 417);
+                    assertOrderTotalsAddUp(order);
+
+                    order.couponCodes = [couponCode];
+                    await orderCalculator.applyPriceAdjustments(ctx, order, [promotion]);
+
+                    expect(order.subTotal).toBe(83);
+                    expect(order.discounts.length).toBe(1);
+                    expect(order.discounts[0].description).toBe('Free shipping');
+                    expect(order.shipping).toBe(0);
+                    expect(order.shippingWithTax).toBe(0);
                     expect(order.total).toBe(order.subTotal);
                     assertOrderTotalsAddUp(order);
                 });

+ 16 - 4
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -334,8 +334,14 @@ export class OrderCalculator {
         if (currentMethodStillEligible) {
             const result = await currentShippingMethod.apply(ctx, order);
             if (result) {
-                shippingLine.price = result.price;
-                shippingLine.priceWithTax = result.priceWithTax;
+                shippingLine.listPrice = result.price;
+                shippingLine.listPriceIncludesTax = result.priceIncludesTax;
+                shippingLine.taxLines = [
+                    {
+                        description: 'shipping tax',
+                        taxRate: result.taxRate,
+                    },
+                ];
             }
             return;
         }
@@ -344,9 +350,15 @@ export class OrderCalculator {
         ]);
         if (results && results.length) {
             const cheapest = results[0];
-            shippingLine.price = cheapest.result.price;
-            shippingLine.priceWithTax = cheapest.result.priceWithTax;
+            shippingLine.listPrice = cheapest.result.price;
+            shippingLine.listPriceIncludesTax = cheapest.result.priceIncludesTax;
             shippingLine.shippingMethod = cheapest.method;
+            shippingLine.taxLines = [
+                {
+                    description: 'shipping tax',
+                    taxRate: cheapest.result.taxRate,
+                },
+            ];
         }
     }
 

+ 26 - 11
packages/core/src/service/services/order-testing.service.ts

@@ -4,11 +4,13 @@ import {
     ShippingMethodQuote,
     TestEligibleShippingMethodsInput,
     TestShippingMethodInput,
+    TestShippingMethodQuote,
     TestShippingMethodResult,
 } from '@vendure/common/lib/generated-types';
 
 import { ID } from '../../../../common/lib/shared-types';
 import { RequestContext } from '../../api/common/request-context';
+import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { ConfigService } from '../../config/config.service';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
@@ -54,9 +56,18 @@ export class OrderTestingService {
         const mockOrder = await this.buildMockOrder(ctx, input.shippingAddress, input.lines);
         const eligible = await shippingMethod.test(ctx, mockOrder);
         const result = eligible ? await shippingMethod.apply(ctx, mockOrder) : undefined;
+        let quote: TestShippingMethodQuote | undefined;
+        if (result) {
+            const { price, priceIncludesTax, taxRate, metadata } = result;
+            quote = {
+                price: priceIncludesTax ? netPriceOf(price, taxRate) : price,
+                priceWithTax: priceIncludesTax ? price : grossPriceOf(price, taxRate),
+                metadata,
+            };
+        }
         return {
             eligible,
-            quote: result,
+            quote,
         };
     }
 
@@ -75,14 +86,17 @@ export class OrderTestingService {
                 translateDeep(result.method, ctx.languageCode);
                 return result;
             })
-            .map(result => ({
-                id: result.method.id,
-                price: result.result.price,
-                priceWithTax: result.result.priceWithTax,
-                name: result.method.name,
-                description: result.method.description,
-                metadata: result.result.metadata,
-            }));
+            .map(result => {
+                const { price, taxRate, priceIncludesTax, metadata } = result.result;
+                return {
+                    id: result.method.id,
+                    price: priceIncludesTax ? netPriceOf(price, taxRate) : price,
+                    priceWithTax: priceIncludesTax ? price : grossPriceOf(price, taxRate),
+                    name: result.method.name,
+                    description: result.method.description,
+                    metadata: result.result.metadata,
+                };
+            });
     }
 
     private async buildMockOrder(
@@ -130,8 +144,9 @@ export class OrderTestingService {
         }
         mockOrder.shippingLines = [
             new ShippingLine({
-                price: 0,
-                priceWithTax: 0,
+                listPrice: 0,
+                listPriceIncludesTax: ctx.channel.pricesIncludeTax,
+                taxLines: [],
                 adjustments: [],
             }),
         ];

+ 15 - 10
packages/core/src/service/services/order.service.ts

@@ -60,6 +60,7 @@ import {
     PaymentDeclinedError,
     PaymentFailedError,
 } from '../../common/error/generated-graphql-shop-errors';
+import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { ListQueryOptions, PaymentMetadata } from '../../common/types/common-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
@@ -522,14 +523,17 @@ export class OrderService {
     async getEligibleShippingMethods(ctx: RequestContext, orderId: ID): Promise<ShippingMethodQuote[]> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, order);
-        return eligibleMethods.map(eligible => ({
-            id: eligible.method.id,
-            price: eligible.result.price,
-            priceWithTax: eligible.result.priceWithTax,
-            description: eligible.method.description,
-            name: eligible.method.name,
-            metadata: eligible.result.metadata,
-        }));
+        return eligibleMethods.map(eligible => {
+            const { price, taxRate, priceIncludesTax, metadata } = eligible.result;
+            return {
+                id: eligible.method.id,
+                price: priceIncludesTax ? netPriceOf(price, taxRate) : price,
+                priceWithTax: priceIncludesTax ? price : grossPriceOf(price, taxRate),
+                description: eligible.method.description,
+                name: eligible.method.name,
+                metadata,
+            };
+        });
     }
 
     async setShippingMethod(
@@ -559,8 +563,9 @@ export class OrderService {
                     shippingMethod,
                     order,
                     adjustments: [],
-                    price: 0,
-                    priceWithTax: 0,
+                    listPrice: 0,
+                    listPriceIncludesTax: ctx.channel.pricesIncludeTax,
+                    taxLines: [],
                 }),
             );
             order.shippingLines = [shippingLine];