Jelajahi Sumber

feat(core): Improve naming of price calculation strategies

Relates to #307.

BREAKING CHANGE: The `TaxCalculationStrategy` has been renamed to
`ProductVariantPriceCalculationStrategy` and moved in the VendureCofig from `taxOptions` to
`catalogOptions` and its API has been simplified.
The `PriceCalculationStrategy` has been renamed to `OrderItemPriceCalculationStrategy`.
Michael Bromley 5 tahun lalu
induk
melakukan
ccbebc9a2a
30 mengubah file dengan 461 tambahan dan 471 penghapusan
  1. 7 2
      packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts
  2. 3 3
      packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts
  3. 1 1
      packages/core/src/api/common/configurable-operation-codec.ts
  4. 1 1
      packages/core/src/api/resolvers/admin/collection.resolver.ts
  5. 5 4
      packages/core/src/app.module.ts
  6. 12 0
      packages/core/src/common/types/common-types.ts
  7. 3 0
      packages/core/src/config/catalog/calculator-test-fixtures.ts
  8. 0 0
      packages/core/src/config/catalog/collection-filter.ts
  9. 0 0
      packages/core/src/config/catalog/default-collection-filters.ts
  10. 184 0
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.spec.ts
  11. 48 0
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.ts
  12. 38 0
      packages/core/src/config/catalog/product-variant-price-calculation-strategy.ts
  13. 5 5
      packages/core/src/config/default-config.ts
  14. 4 3
      packages/core/src/config/index.ts
  15. 5 4
      packages/core/src/config/order/default-order-item-price-calculation-strategy.ts
  16. 17 18
      packages/core/src/config/order/order-item-price-calculation-strategy.ts
  17. 0 44
      packages/core/src/config/tax/default-tax-calculation-strategy.ts
  18. 0 30
      packages/core/src/config/tax/tax-calculation-strategy.ts
  19. 12 11
      packages/core/src/config/vendure-config.ts
  20. 10 14
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  21. 0 2
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  22. 0 91
      packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts
  23. 0 178
      packages/core/src/service/helpers/tax-calculator/tax-calculator.spec.ts
  24. 0 47
      packages/core/src/service/helpers/tax-calculator/tax-calculator.ts
  25. 0 2
      packages/core/src/service/service.module.ts
  26. 1 1
      packages/core/src/service/services/collection.service.ts
  27. 2 2
      packages/core/src/service/services/order-testing.service.ts
  28. 5 2
      packages/core/src/service/services/order.service.ts
  29. 5 6
      packages/core/src/service/services/product-variant.service.ts
  30. 93 0
      packages/core/src/testing/order-test-utils.ts

+ 7 - 2
packages/core/e2e/fixtures/test-price-calculation-strategy.ts → packages/core/e2e/fixtures/test-order-item-price-calculation-strategy.ts

@@ -1,9 +1,14 @@
-import { CalculatedPrice, PriceCalculationStrategy, ProductVariant, RequestContext } from '@vendure/core';
+import {
+    CalculatedPrice,
+    OrderItemPriceCalculationStrategy,
+    ProductVariant,
+    RequestContext,
+} from '@vendure/core';
 
 /**
  * Adds $5 for items with gift wrapping.
  */
-export class TestPriceCalculationStrategy implements PriceCalculationStrategy {
+export class TestOrderItemPriceCalculationStrategy implements OrderItemPriceCalculationStrategy {
     calculateUnitPrice(
         ctx: RequestContext,
         productVariant: ProductVariant,

+ 3 - 3
packages/core/e2e/price-calculation-strategy.e2e-spec.ts → packages/core/e2e/order-item-price-calculation-strategy.e2e-spec.ts

@@ -6,11 +6,11 @@ import path from 'path';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
-import { TestPriceCalculationStrategy } from './fixtures/test-price-calculation-strategy';
+import { TestOrderItemPriceCalculationStrategy } from './fixtures/test-order-item-price-calculation-strategy';
 import { AddItemToOrder, SearchProductsShop, SinglePrice } from './graphql/generated-e2e-shop-types';
 import { ADD_ITEM_TO_ORDER, SEARCH_PRODUCTS_SHOP } from './graphql/shop-definitions';
 
-describe('custom PriceCalculationStrategy', () => {
+describe('custom OrderItemPriceCalculationStrategy', () => {
     let variants: SearchProductsShop.Items[];
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig, {
@@ -18,7 +18,7 @@ describe('custom PriceCalculationStrategy', () => {
                 OrderLine: [{ name: 'giftWrap', type: 'boolean' }],
             },
             orderOptions: {
-                priceCalculationStrategy: new TestPriceCalculationStrategy(),
+                orderItemPriceCalculationStrategy: new TestOrderItemPriceCalculationStrategy(),
             },
             plugins: [DefaultSearchPlugin],
         }),

+ 1 - 1
packages/core/src/api/common/configurable-operation-codec.ts

@@ -11,7 +11,7 @@ import {
     ShippingCalculator,
     ShippingEligibilityChecker,
 } from '../../config';
-import { CollectionFilter } from '../../config/collection/collection-filter';
+import { CollectionFilter } from '../../config/catalog/collection-filter';
 import { ConfigService } from '../../config/config.service';
 import { PaymentMethodHandler } from '../../config/payment-method/payment-method-handler';
 

+ 1 - 1
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -14,7 +14,7 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { UserInputError } from '../../../common/error/errors';
 import { Translated } from '../../../common/types/locale-types';
-import { CollectionFilter } from '../../../config/collection/collection-filter';
+import { CollectionFilter } from '../../../config/catalog/collection-filter';
 import { Collection } from '../../../entity/collection/collection.entity';
 import { CollectionService } from '../../../service/services/collection.service';
 import { FacetValueService } from '../../../service/services/facet-value.service';

+ 5 - 4
packages/core/src/app.module.ts

@@ -118,13 +118,14 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
             assetPreviewStrategy,
             assetStorageStrategy,
         } = this.configService.assetOptions;
+        const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
         const { adminAuthenticationStrategy, shopAuthenticationStrategy } = this.configService.authOptions;
-        const { taxCalculationStrategy, taxZoneStrategy } = this.configService.taxOptions;
+        const { taxZoneStrategy } = this.configService.taxOptions;
         const { jobQueueStrategy } = this.configService.jobQueueOptions;
         const {
             mergeStrategy,
             checkoutMergeStrategy,
-            priceCalculationStrategy,
+            orderItemPriceCalculationStrategy,
             process,
             orderCodeStrategy,
             stockAllocationStrategy,
@@ -136,14 +137,14 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
             assetNamingStrategy,
             assetPreviewStrategy,
             assetStorageStrategy,
-            taxCalculationStrategy,
             taxZoneStrategy,
             jobQueueStrategy,
             mergeStrategy,
             checkoutMergeStrategy,
             orderCodeStrategy,
             entityIdStrategy,
-            priceCalculationStrategy,
+            productVariantPriceCalculationStrategy,
+            orderItemPriceCalculationStrategy,
             ...process,
             stockAllocationStrategy,
         ];

+ 12 - 0
packages/core/src/common/types/common-types.ts

@@ -121,3 +121,15 @@ export type PaymentMetadata = {
 } & {
     public?: any;
 };
+
+/**
+ * @description
+ * The result of the price calculation from the {@link ProductVariantPriceCalculationStrategy} or the
+ * {@link OrderItemPriceCalculationStrategy}.
+ *
+ * @docsCategory Common
+ */
+export type PriceCalculationResult = {
+    price: number;
+    priceIncludesTax: boolean;
+};

+ 3 - 0
packages/core/src/config/catalog/calculator-test-fixtures.ts

@@ -0,0 +1,3 @@
+import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
+import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
+import { Zone } from '../../entity/zone/zone.entity';

+ 0 - 0
packages/core/src/config/collection/collection-filter.ts → packages/core/src/config/catalog/collection-filter.ts


+ 0 - 0
packages/core/src/config/collection/default-collection-filters.ts → packages/core/src/config/catalog/default-collection-filters.ts


+ 184 - 0
packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.spec.ts

@@ -0,0 +1,184 @@
+import {
+    createRequestContext,
+    MockTaxRateService,
+    taxCategoryReduced,
+    taxCategoryStandard,
+    taxRateDefaultReduced,
+    taxRateDefaultStandard,
+    taxRateOtherReduced,
+    taxRateOtherStandard,
+    zoneDefault,
+    zoneOther,
+    zoneWithNoTaxRate,
+} from '../../testing/order-test-utils';
+
+import { DefaultProductVariantPriceCalculationStrategy } from './default-product-variant-price-calculation-strategy';
+
+describe('DefaultProductVariantPriceCalculationStrategy', () => {
+    let strategy: DefaultProductVariantPriceCalculationStrategy;
+    const inputPrice = 6543;
+
+    beforeEach(async () => {
+        strategy = new DefaultProductVariantPriceCalculationStrategy();
+        const mockInjector = {
+            get: () => {
+                return new MockTaxRateService();
+            },
+        } as any;
+        strategy.init(mockInjector);
+    });
+
+    describe('with prices which do not include tax', () => {
+        it('standard tax, default zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: false });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryStandard,
+                activeTaxZone: zoneDefault,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: false,
+            });
+        });
+
+        it('reduced tax, default zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: false });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryReduced,
+                activeTaxZone: zoneDefault,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: false,
+            });
+        });
+
+        it('standard tax, other zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: false });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryStandard,
+                activeTaxZone: zoneOther,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: false,
+            });
+        });
+
+        it('reduced tax, other zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: false });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryReduced,
+                activeTaxZone: zoneOther,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: false,
+            });
+        });
+
+        it('standard tax, unconfigured zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: false });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryReduced,
+                activeTaxZone: zoneWithNoTaxRate,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: false,
+            });
+        });
+    });
+
+    describe('with prices which include tax', () => {
+        it('standard tax, default zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryStandard,
+                activeTaxZone: zoneDefault,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: true,
+            });
+        });
+
+        it('reduced tax, default zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryReduced,
+                activeTaxZone: zoneDefault,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: inputPrice,
+                priceIncludesTax: true,
+            });
+        });
+
+        it('standard tax, other zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryStandard,
+                activeTaxZone: zoneOther,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: taxRateDefaultStandard.netPriceOf(inputPrice),
+                priceIncludesTax: false,
+            });
+        });
+
+        it('reduced tax, other zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryReduced,
+                activeTaxZone: zoneOther,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: taxRateDefaultReduced.netPriceOf(inputPrice),
+                priceIncludesTax: false,
+            });
+        });
+
+        it('standard tax, unconfigured zone', () => {
+            const ctx = createRequestContext({ pricesIncludeTax: true });
+            const result = strategy.calculate({
+                inputPrice,
+                taxCategory: taxCategoryStandard,
+                activeTaxZone: zoneWithNoTaxRate,
+                ctx,
+            });
+
+            expect(result).toEqual({
+                price: taxRateDefaultStandard.netPriceOf(inputPrice),
+                priceIncludesTax: false,
+            });
+        });
+    });
+});

+ 48 - 0
packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.ts

@@ -0,0 +1,48 @@
+import { Injector } from '../../common/injector';
+import { PriceCalculationResult } from '../../common/types/common-types';
+import { idsAreEqual } from '../../common/utils';
+import { TaxRateService } from '../../service/services/tax-rate.service';
+
+import {
+    ProductVariantPriceCalculationArgs,
+    ProductVariantPriceCalculationStrategy,
+} from './product-variant-price-calculation-strategy';
+
+/**
+ * @description
+ * A default ProductVariant price calculation function.
+ *
+ * @docsCategory tax
+ */
+export class DefaultProductVariantPriceCalculationStrategy implements ProductVariantPriceCalculationStrategy {
+    private taxRateService: TaxRateService;
+
+    init(injector: Injector) {
+        this.taxRateService = injector.get(TaxRateService);
+    }
+
+    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult {
+        const { inputPrice, activeTaxZone, ctx, taxCategory } = args;
+        let price = inputPrice;
+        let priceIncludesTax = false;
+        const taxRate = this.taxRateService.getApplicableTaxRate(activeTaxZone, taxCategory);
+
+        if (ctx.channel.pricesIncludeTax) {
+            const isDefaultZone = idsAreEqual(activeTaxZone.id, ctx.channel.defaultTaxZone.id);
+            if (isDefaultZone) {
+                priceIncludesTax = true;
+            } else {
+                const taxRateForDefaultZone = this.taxRateService.getApplicableTaxRate(
+                    ctx.channel.defaultTaxZone,
+                    taxCategory,
+                );
+                price = taxRateForDefaultZone.netPriceOf(inputPrice);
+            }
+        }
+
+        return {
+            price,
+            priceIncludesTax,
+        };
+    }
+}

+ 38 - 0
packages/core/src/config/catalog/product-variant-price-calculation-strategy.ts

@@ -0,0 +1,38 @@
+import { RequestContext } from '../../api/common/request-context';
+import { PriceCalculationResult } from '../../common/types/common-types';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { TaxCategory, Zone } from '../../entity/index';
+import { TaxRateService } from '../../service/services/tax-rate.service';
+
+/**
+ * @description
+ * Defines how ProductVariant are calculated based on the input price, tax zone and current request context.
+ *
+ * @docsCategory tax
+ */
+export interface ProductVariantPriceCalculationStrategy extends InjectableStrategy {
+    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult;
+}
+
+/**
+ * @description
+ * The arguments passed the the `calculate` method of the configured {@link ProductVariantPriceCalculationStrategy}.
+ *
+ * @docsCategory tax
+ * @docsPage Tax Types
+ */
+export interface ProductVariantPriceCalculationArgs {
+    inputPrice: number;
+    taxCategory: TaxCategory;
+    activeTaxZone: Zone;
+    ctx: RequestContext;
+}
+
+/**
+ * @description
+ * This is an alias of {@link ProductVariantPriceCalculationStrategy} to preserve compatibility when upgrading.
+ *
+ * @deprecated Use ProductVariantPriceCalculationStrategy
+ * @docsCategory Orders
+ */
+export interface TaxCalculationStrategy extends ProductVariantPriceCalculationStrategy {}

+ 5 - 5
packages/core/src/config/default-config.ts

@@ -12,11 +12,12 @@ import { DefaultAssetNamingStrategy } from './asset-naming-strategy/default-asse
 import { NoAssetPreviewStrategy } from './asset-preview-strategy/no-asset-preview-strategy';
 import { NoAssetStorageStrategy } from './asset-storage-strategy/no-asset-storage-strategy';
 import { NativeAuthenticationStrategy } from './auth/native-authentication-strategy';
-import { defaultCollectionFilters } from './collection/default-collection-filters';
+import { defaultCollectionFilters } from './catalog/default-collection-filters';
+import { DefaultProductVariantPriceCalculationStrategy } from './catalog/default-product-variant-price-calculation-strategy';
 import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
 import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handler';
 import { DefaultLogger } from './logger/default-logger';
-import { DefaultPriceCalculationStrategy } from './order/default-price-calculation-strategy';
+import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-item-price-calculation-strategy';
 import { DefaultStockAllocationStrategy } from './order/default-stock-allocation-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
 import { DefaultOrderCodeStrategy } from './order/order-code-strategy';
@@ -25,7 +26,6 @@ import { defaultPromotionActions, defaultPromotionConditions } from './promotion
 import { InMemorySessionCacheStrategy } from './session-cache/in-memory-session-cache-strategy';
 import { defaultShippingCalculator } from './shipping-method/default-shipping-calculator';
 import { defaultShippingEligibilityChecker } from './shipping-method/default-shipping-eligibility-checker';
-import { DefaultTaxCalculationStrategy } from './tax/default-tax-calculation-strategy';
 import { DefaultTaxZoneStrategy } from './tax/default-tax-zone-strategy';
 import { RuntimeVendureConfig } from './vendure-config';
 
@@ -80,6 +80,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     },
     catalogOptions: {
         collectionFilters: defaultCollectionFilters,
+        productVariantPriceCalculationStrategy: new DefaultProductVariantPriceCalculationStrategy(),
     },
     entityIdStrategy: new AutoIncrementIdStrategy(),
     assetOptions: {
@@ -105,7 +106,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     },
     orderOptions: {
         orderItemsLimit: 999,
-        priceCalculationStrategy: new DefaultPriceCalculationStrategy(),
+        orderItemPriceCalculationStrategy: new DefaultOrderItemPriceCalculationStrategy(),
         mergeStrategy: new MergeOrdersStrategy(),
         checkoutMergeStrategy: new UseGuestStrategy(),
         process: [],
@@ -117,7 +118,6 @@ export const defaultConfig: RuntimeVendureConfig = {
     },
     taxOptions: {
         taxZoneStrategy: new DefaultTaxZoneStrategy(),
-        taxCalculationStrategy: new DefaultTaxCalculationStrategy(),
     },
     importExportOptions: {
         importAssetsDir: __dirname,

+ 4 - 3
packages/core/src/config/index.ts

@@ -4,8 +4,8 @@ export * from './asset-preview-strategy/asset-preview-strategy';
 export * from './asset-storage-strategy/asset-storage-strategy';
 export * from './auth/authentication-strategy';
 export * from './auth/native-authentication-strategy';
-export * from './collection/collection-filter';
-export * from './collection/default-collection-filters';
+export * from './catalog/collection-filter';
+export * from './catalog/default-collection-filters';
 export * from './config.module';
 export * from './config.service';
 export * from './custom-field/custom-field-types';
@@ -23,7 +23,7 @@ export * from './merge-config';
 export * from './order/custom-order-process';
 export * from './order/order-code-strategy';
 export * from './order/order-merge-strategy';
-export * from './order/price-calculation-strategy';
+export * from './order/order-item-price-calculation-strategy';
 export * from './order/stock-allocation-strategy';
 export * from './payment-method/example-payment-method-handler';
 export * from './payment-method/payment-method-handler';
@@ -36,3 +36,4 @@ export * from './shipping-method/default-shipping-eligibility-checker';
 export * from './shipping-method/shipping-calculator';
 export * from './shipping-method/shipping-eligibility-checker';
 export * from './vendure-config';
+export { PriceCalculationResult } from '../common/types/common-types';

+ 5 - 4
packages/core/src/config/order/default-price-calculation-strategy.ts → packages/core/src/config/order/default-order-item-price-calculation-strategy.ts

@@ -1,20 +1,21 @@
 import { RequestContext } from '../../api/common/request-context';
+import { PriceCalculationResult } from '../../common/types/common-types';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 
-import { CalculatedPrice, PriceCalculationStrategy } from './price-calculation-strategy';
+import { OrderItemPriceCalculationStrategy } from './order-item-price-calculation-strategy';
 
 /**
  * @description
- * The default {@link PriceCalculationStrategy}, which simply passes through the price of
+ * The default {@link OrderItemPriceCalculationStrategy}, which simply passes through the price of
  * the ProductVariant without performing any calculations
  *
  * @docsCategory orders
  */
-export class DefaultPriceCalculationStrategy implements PriceCalculationStrategy {
+export class DefaultOrderItemPriceCalculationStrategy implements OrderItemPriceCalculationStrategy {
     calculateUnitPrice(
         ctx: RequestContext,
         productVariant: ProductVariant,
-    ): CalculatedPrice | Promise<CalculatedPrice> {
+    ): PriceCalculationResult | Promise<PriceCalculationResult> {
         return {
             price: productVariant.listPrice,
             priceIncludesTax: productVariant.listPriceIncludesTax,

+ 17 - 18
packages/core/src/config/order/price-calculation-strategy.ts → packages/core/src/config/order/order-item-price-calculation-strategy.ts

@@ -1,28 +1,18 @@
 import { RequestContext } from '../../api/common/request-context';
+import { PriceCalculationResult } from '../../common/types/common-types';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 
 /**
  * @description
- * The result of the price calculation from the {@link PriceCalculationStrategy}.
- *
- * @docsCategory Orders
- */
-export type CalculatedPrice = {
-    price: number;
-    priceIncludesTax: boolean;
-};
-
-/**
- * @description
- * The PriceCalculationStrategy defines the price of an OrderItem when a ProductVariant gets added
+ * The OrderItemPriceCalculationStrategy defines the price of an OrderItem when a ProductVariant gets added
  * to an order via the `addItemToOrder` mutation. By default the {@link DefaultPriceCalculationStrategy}
  * is used.
  *
- * ### PriceCalculationStrategy vs Promotions
- * Both the PriceCalculationStrategy and Promotions can be used to alter the price paid for a product.
+ * ### OrderItemPriceCalculationStrategy vs Promotions
+ * Both the OrderItemPriceCalculationStrategy and Promotions can be used to alter the price paid for a product.
  *
- * Use PriceCalculationStrategy if:
+ * Use OrderItemPriceCalculationStrategy if:
  *
  * * The price is not dependent on quantity or on the other contents of the Order.
  * * The price calculation is based on the properties of the ProductVariant and any CustomFields
@@ -39,7 +29,7 @@ export type CalculatedPrice = {
  *
  * ### Example use-cases
  *
- * A custom PriceCalculationStrategy can be used to implement things like:
+ * A custom OrderItemPriceCalculationStrategy can be used to implement things like:
  *
  * * A gift-wrapping service, where a boolean custom field is defined on the OrderLine. If `true`,
  *   a gift-wrapping surcharge would be added to the price.
@@ -48,7 +38,7 @@ export type CalculatedPrice = {
  *
  * @docsCategory Orders
  */
-export interface PriceCalculationStrategy extends InjectableStrategy {
+export interface OrderItemPriceCalculationStrategy extends InjectableStrategy {
     /**
      * @description
      * Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
@@ -58,5 +48,14 @@ export interface PriceCalculationStrategy extends InjectableStrategy {
         ctx: RequestContext,
         productVariant: ProductVariant,
         orderLineCustomFields: { [key: string]: any },
-    ): CalculatedPrice | Promise<CalculatedPrice>;
+    ): PriceCalculationResult | Promise<PriceCalculationResult>;
 }
+
+/**
+ * @description
+ * This is an alias of {@link OrderItemPriceCalculationStrategy} to preserve compatibility when upgrading.
+ *
+ * @deprecated Use OrderItemPriceCalculationStrategy
+ * @docsCategory Orders
+ */
+export interface PriceCalculationStrategy extends OrderItemPriceCalculationStrategy {}

+ 0 - 44
packages/core/src/config/tax/default-tax-calculation-strategy.ts

@@ -1,44 +0,0 @@
-import { RequestContext } from '../../api/common/request-context';
-import { idsAreEqual } from '../../common/utils';
-import { TaxCategory } from '../../entity';
-import { TaxCalculationResult } from '../../service/helpers/tax-calculator/tax-calculator';
-import { TaxRateService } from '../../service/services/tax-rate.service';
-
-import { TaxCalculationArgs, TaxCalculationStrategy } from './tax-calculation-strategy';
-
-/**
- * @description
- * A default tax calculation function.
- *
- * @docsCategory tax
- */
-export class DefaultTaxCalculationStrategy implements TaxCalculationStrategy {
-    calculate(args: TaxCalculationArgs): TaxCalculationResult {
-        const { inputPrice, activeTaxZone, ctx, taxCategory, taxRateService } = args;
-        let price = 0;
-        let priceIncludesTax = false;
-        const taxRate = taxRateService.getApplicableTaxRate(activeTaxZone, taxCategory);
-
-        if (ctx.channel.pricesIncludeTax) {
-            const isDefaultZone = idsAreEqual(activeTaxZone.id, ctx.channel.defaultTaxZone.id);
-            const taxRateForDefaultZone = taxRateService.getApplicableTaxRate(
-                ctx.channel.defaultTaxZone,
-                taxCategory,
-            );
-
-            if (isDefaultZone) {
-                priceIncludesTax = true;
-                price = inputPrice;
-            } else {
-                price = taxRateForDefaultZone.netPriceOf(inputPrice);
-            }
-        } else {
-            price = inputPrice;
-        }
-
-        return {
-            price,
-            priceIncludesTax,
-        };
-    }
-}

+ 0 - 30
packages/core/src/config/tax/tax-calculation-strategy.ts

@@ -1,30 +0,0 @@
-import { RequestContext } from '../../api/common/request-context';
-import { InjectableStrategy } from '../../common/types/injectable-strategy';
-import { TaxCategory, Zone } from '../../entity';
-import { TaxCalculationResult } from '../../service/helpers/tax-calculator/tax-calculator';
-import { TaxRateService } from '../../service/services/tax-rate.service';
-
-/**
- * @description
- * Defines how taxes are calculated based on the input price, tax zone and current request context.
- *
- * @docsCategory tax
- */
-export interface TaxCalculationStrategy extends InjectableStrategy {
-    calculate(args: TaxCalculationArgs): TaxCalculationResult;
-}
-
-/**
- * @description
- * The arguments passed the the `calculate` method of the configured {@link TaxCalculationStrategy}.
- *
- * @docsCategory tax
- * @docsPage Tax Types
- */
-export interface TaxCalculationArgs {
-    inputPrice: number;
-    taxCategory: TaxCategory;
-    activeTaxZone: Zone;
-    ctx: RequestContext;
-    taxRateService: TaxRateService;
-}

+ 12 - 11
packages/core/src/config/vendure-config.ts

@@ -12,7 +12,8 @@ import { AssetNamingStrategy } from './asset-naming-strategy/asset-naming-strate
 import { AssetPreviewStrategy } from './asset-preview-strategy/asset-preview-strategy';
 import { AssetStorageStrategy } from './asset-storage-strategy/asset-storage-strategy';
 import { AuthenticationStrategy } from './auth/authentication-strategy';
-import { CollectionFilter } from './collection/collection-filter';
+import { CollectionFilter } from './catalog/collection-filter';
+import { ProductVariantPriceCalculationStrategy } from './catalog/product-variant-price-calculation-strategy';
 import { CustomFields } from './custom-field/custom-field-types';
 import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
 import { CustomFulfillmentProcess } from './fulfillment/custom-fulfillment-process';
@@ -21,8 +22,8 @@ import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { CustomOrderProcess } from './order/custom-order-process';
 import { OrderCodeStrategy } from './order/order-code-strategy';
+import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
-import { PriceCalculationStrategy } from './order/price-calculation-strategy';
 import { StockAllocationStrategy } from './order/stock-allocation-strategy';
 import { PaymentMethodHandler } from './payment-method/payment-method-handler';
 import { PromotionAction } from './promotion/promotion-action';
@@ -30,7 +31,6 @@ import { PromotionCondition } from './promotion/promotion-condition';
 import { SessionCacheStrategy } from './session-cache/session-cache-strategy';
 import { ShippingCalculator } from './shipping-method/shipping-calculator';
 import { ShippingEligibilityChecker } from './shipping-method/shipping-eligibility-checker';
-import { TaxCalculationStrategy } from './tax/tax-calculation-strategy';
 import { TaxZoneStrategy } from './tax/tax-zone-strategy';
 
 /**
@@ -386,7 +386,7 @@ export interface OrderOptions {
      *
      * @default DefaultPriceCalculationStrategy
      */
-    priceCalculationStrategy?: PriceCalculationStrategy;
+    orderItemPriceCalculationStrategy?: OrderItemPriceCalculationStrategy;
     /**
      * @description
      * Allows the definition of custom states and transition logic for the order process state machine.
@@ -496,6 +496,14 @@ export interface CatalogOptions {
      * @default defaultCollectionFilters
      */
     collectionFilters: Array<CollectionFilter<any>>;
+    /**
+     * @description
+     * Defines the strategy used for calculating the price of ProductVariants based
+     * on the Channel settings and active tax Zone.
+     *
+     * @default DefaultTaxCalculationStrategy
+     */
+    productVariantPriceCalculationStrategy: ProductVariantPriceCalculationStrategy;
 }
 
 /**
@@ -592,13 +600,6 @@ export interface TaxOptions {
      * @default DefaultTaxZoneStrategy
      */
     taxZoneStrategy: TaxZoneStrategy;
-    /**
-     * @description
-     * Defines the strategy used for calculating taxes.
-     *
-     * @default DefaultTaxCalculationStrategy
-     */
-    taxCalculationStrategy: TaxCalculationStrategy;
 }
 
 /**

+ 10 - 14
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -5,32 +5,30 @@ import { summate } from '@vendure/common/lib/shared-utils';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { PromotionItemAction, PromotionOrderAction, PromotionShippingAction } from '../../../config';
+import { DefaultProductVariantPriceCalculationStrategy } from '../../../config/catalog/default-product-variant-price-calculation-strategy';
 import { ConfigService } from '../../../config/config.service';
 import { MockConfigService } from '../../../config/config.service.mock';
 import { PromotionCondition } from '../../../config/promotion/promotion-condition';
-import { DefaultTaxCalculationStrategy } from '../../../config/tax/default-tax-calculation-strategy';
 import { DefaultTaxZoneStrategy } from '../../../config/tax/default-tax-zone-strategy';
-import { Promotion, ShippingMethod } from '../../../entity';
+import { Promotion } from '../../../entity';
 import { OrderItem } from '../../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { ShippingLine } from '../../../entity/shipping-line/shipping-line.entity';
 import { TaxCategory } from '../../../entity/tax-category/tax-category.entity';
 import { EventBus } from '../../../event-bus/event-bus';
+import {
+    createRequestContext,
+    MockTaxRateService,
+    taxCategoryReduced,
+    taxCategoryStandard,
+} from '../../../testing/order-test-utils';
 import { WorkerService } from '../../../worker/worker.service';
 import { ShippingMethodService } from '../../services/shipping-method.service';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { ZoneService } from '../../services/zone.service';
-import { TransactionalConnection } from '../../transaction/transactional-connection';
 import { ListQueryBuilder } from '../list-query-builder/list-query-builder';
 import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
-import { TaxCalculator } from '../tax-calculator/tax-calculator';
-import {
-    createRequestContext,
-    MockConnection,
-    taxCategoryReduced,
-    taxCategoryStandard,
-} from '../tax-calculator/tax-calculator-test-fixtures';
 
 import { OrderCalculator } from './order-calculator';
 
@@ -56,14 +54,12 @@ describe('OrderCalculator', () => {
         const module = await Test.createTestingModule({
             providers: [
                 OrderCalculator,
-                TaxCalculator,
-                TaxRateService,
+                { provide: TaxRateService, useClass: MockTaxRateService },
                 { provide: ShippingCalculator, useValue: { getEligibleShippingMethods: () => [] } },
                 {
                     provide: ShippingMethodService,
                     useValue: { findOne: (ctx: RequestContext) => createMockShippingMethod(ctx) },
                 },
-                { provide: TransactionalConnection, useClass: MockConnection },
                 { provide: ListQueryBuilder, useValue: {} },
                 { provide: ConfigService, useClass: MockConfigService },
                 { provide: EventBus, useValue: { publish: () => ({}) } },
@@ -76,7 +72,7 @@ describe('OrderCalculator', () => {
         const mockConfigService = module.get<ConfigService, MockConfigService>(ConfigService);
         mockConfigService.taxOptions = {
             taxZoneStrategy: new DefaultTaxZoneStrategy(),
-            taxCalculationStrategy: new DefaultTaxCalculationStrategy(),
+            taxCalculationStrategy: new DefaultProductVariantPriceCalculationStrategy(),
         };
         const taxRateService = module.get(TaxRateService);
         await taxRateService.initTaxRates();

+ 0 - 2
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -16,7 +16,6 @@ import { ShippingMethodService } from '../../services/shipping-method.service';
 import { TaxRateService } from '../../services/tax-rate.service';
 import { ZoneService } from '../../services/zone.service';
 import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
-import { TaxCalculator } from '../tax-calculator/tax-calculator';
 
 import { prorate } from './prorate';
 
@@ -26,7 +25,6 @@ export class OrderCalculator {
         private configService: ConfigService,
         private zoneService: ZoneService,
         private taxRateService: TaxRateService,
-        private taxCalculator: TaxCalculator,
         private shippingMethodService: ShippingMethodService,
         private shippingCalculator: ShippingCalculator,
     ) {}

+ 0 - 91
packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts

@@ -1,91 +0,0 @@
-import { LanguageCode } from '@vendure/common/lib/generated-types';
-
-import { RequestContext } from '../../../api/common/request-context';
-import { Channel } from '../../../entity/channel/channel.entity';
-import { TaxCategory } from '../../../entity/tax-category/tax-category.entity';
-import { TaxRate } from '../../../entity/tax-rate/tax-rate.entity';
-import { Zone } from '../../../entity/zone/zone.entity';
-
-export const taxCategoryStandard = new TaxCategory({
-    id: 'taxCategoryStandard',
-    name: 'Standard Tax',
-});
-export const taxCategoryReduced = new TaxCategory({
-    id: 'taxCategoryReduced',
-    name: 'Reduced Tax',
-});
-export const zoneDefault = new Zone({
-    id: 'zoneDefault',
-    name: 'Default Zone',
-});
-export const zoneOther = new Zone({
-    id: 'zoneOther',
-    name: 'Other Zone',
-});
-export const zoneWithNoTaxRate = new Zone({
-    id: 'zoneWithNoTaxRate',
-    name: 'Zone for which no TaxRate is configured',
-});
-export const taxRateDefaultStandard = new TaxRate({
-    id: 'taxRateDefaultStandard',
-    name: 'Default Standard',
-    value: 20,
-    enabled: true,
-    zone: zoneDefault,
-    category: taxCategoryStandard,
-});
-export const taxRateDefaultReduced = new TaxRate({
-    id: 'taxRateDefaultReduced',
-    name: 'Default Reduced',
-    value: 10,
-    enabled: true,
-    zone: zoneDefault,
-    category: taxCategoryReduced,
-});
-export const taxRateOtherStandard = new TaxRate({
-    id: 'taxRateOtherStandard',
-    name: 'Other Standard',
-    value: 15,
-    enabled: true,
-    zone: zoneOther,
-    category: taxCategoryStandard,
-});
-export const taxRateOtherReduced = new TaxRate({
-    id: 'taxRateOtherReduced',
-    name: 'Other Reduced',
-    value: 5,
-    enabled: true,
-    zone: zoneOther,
-    category: taxCategoryReduced,
-});
-
-export class MockConnection {
-    getRepository() {
-        return {
-            find() {
-                return Promise.resolve([
-                    taxRateDefaultStandard,
-                    taxRateDefaultReduced,
-                    taxRateOtherStandard,
-                    taxRateOtherReduced,
-                ]);
-            },
-        };
-    }
-}
-
-export function createRequestContext(options: { pricesIncludeTax: boolean }): RequestContext {
-    const channel = new Channel({
-        defaultTaxZone: zoneDefault,
-        pricesIncludeTax: options.pricesIncludeTax,
-    });
-    const ctx = new RequestContext({
-        apiType: 'admin',
-        channel,
-        authorizedAsOwnerOnly: false,
-        languageCode: LanguageCode.en,
-        isAuthorized: true,
-        session: {} as any,
-    });
-    return ctx;
-}

+ 0 - 178
packages/core/src/service/helpers/tax-calculator/tax-calculator.spec.ts

@@ -1,178 +0,0 @@
-import { Test } from '@nestjs/testing';
-
-import { ConfigService } from '../../../config/config.service';
-import { MockConfigService } from '../../../config/config.service.mock';
-import { DefaultTaxCalculationStrategy } from '../../../config/tax/default-tax-calculation-strategy';
-import { EventBus } from '../../../event-bus/event-bus';
-import { WorkerService } from '../../../worker/worker.service';
-import { TaxRateService } from '../../services/tax-rate.service';
-import { TransactionalConnection } from '../../transaction/transactional-connection';
-import { ListQueryBuilder } from '../list-query-builder/list-query-builder';
-
-import { TaxCalculator } from './tax-calculator';
-import {
-    createRequestContext,
-    MockConnection,
-    taxCategoryReduced,
-    taxCategoryStandard,
-    taxRateDefaultReduced,
-    taxRateDefaultStandard,
-    taxRateOtherReduced,
-    taxRateOtherStandard,
-    zoneDefault,
-    zoneOther,
-    zoneWithNoTaxRate,
-} from './tax-calculator-test-fixtures';
-
-describe('TaxCalculator', () => {
-    let taxCalculator: TaxCalculator;
-    const inputPrice = 6543;
-
-    beforeEach(async () => {
-        const module = await Test.createTestingModule({
-            providers: [
-                TaxCalculator,
-                TaxRateService,
-                { provide: ConfigService, useClass: MockConfigService },
-                { provide: TransactionalConnection, useClass: MockConnection },
-                { provide: ListQueryBuilder, useValue: {} },
-                { provide: EventBus, useValue: { publish: () => ({}) } },
-                { provide: WorkerService, useValue: { send: () => ({}) } },
-            ],
-        }).compile();
-
-        taxCalculator = module.get(TaxCalculator);
-        const taxRateService = module.get(TaxRateService);
-        const mockConfigService = module.get<ConfigService, MockConfigService>(ConfigService);
-        mockConfigService.taxOptions = {
-            taxCalculationStrategy: new DefaultTaxCalculationStrategy(),
-        };
-        await taxRateService.initTaxRates();
-    });
-
-    describe('with prices which do not include tax', () => {
-        it('standard tax, default zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryStandard, zoneDefault, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: false,
-                priceWithTax: taxRateDefaultStandard.grossPriceOf(inputPrice),
-                priceWithoutTax: inputPrice,
-            });
-        });
-
-        it('reduced tax, default zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = taxCalculator.calculate(6543, taxCategoryReduced, zoneDefault, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: false,
-                priceWithTax: taxRateDefaultReduced.grossPriceOf(inputPrice),
-                priceWithoutTax: inputPrice,
-            });
-        });
-
-        it('standard tax, other zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = taxCalculator.calculate(6543, taxCategoryStandard, zoneOther, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: false,
-                priceWithTax: taxRateOtherStandard.grossPriceOf(inputPrice),
-                priceWithoutTax: inputPrice,
-            });
-        });
-
-        it('reduced tax, other zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryReduced, zoneOther, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: false,
-                priceWithTax: taxRateOtherReduced.grossPriceOf(inputPrice),
-                priceWithoutTax: inputPrice,
-            });
-        });
-
-        it('standard tax, unconfigured zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryReduced, zoneWithNoTaxRate, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: false,
-                priceWithTax: inputPrice,
-                priceWithoutTax: inputPrice,
-            });
-        });
-    });
-
-    describe('with prices which include tax', () => {
-        it('standard tax, default zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryStandard, zoneDefault, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: true,
-                priceWithTax: inputPrice,
-                priceWithoutTax: taxRateDefaultStandard.netPriceOf(inputPrice),
-            });
-        });
-
-        it('reduced tax, default zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryReduced, zoneDefault, ctx);
-
-            expect(result).toEqual({
-                price: inputPrice,
-                priceIncludesTax: true,
-                priceWithTax: inputPrice,
-                priceWithoutTax: taxRateDefaultReduced.netPriceOf(inputPrice),
-            });
-        });
-
-        it('standard tax, other zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryStandard, zoneOther, ctx);
-
-            expect(result).toEqual({
-                price: taxRateDefaultStandard.netPriceOf(inputPrice),
-                priceIncludesTax: false,
-                priceWithTax: taxRateOtherStandard.grossPriceOf(
-                    taxRateDefaultStandard.netPriceOf(inputPrice),
-                ),
-                priceWithoutTax: taxRateDefaultStandard.netPriceOf(inputPrice),
-            });
-        });
-
-        it('reduced tax, other zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryReduced, zoneOther, ctx);
-
-            expect(result).toEqual({
-                price: taxRateDefaultReduced.netPriceOf(inputPrice),
-                priceIncludesTax: false,
-                priceWithTax: taxRateOtherReduced.grossPriceOf(taxRateDefaultReduced.netPriceOf(inputPrice)),
-                priceWithoutTax: taxRateDefaultReduced.netPriceOf(inputPrice),
-            });
-        });
-
-        it('standard tax, unconfigured zone', () => {
-            const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = taxCalculator.calculate(inputPrice, taxCategoryStandard, zoneWithNoTaxRate, ctx);
-
-            expect(result).toEqual({
-                price: taxRateDefaultStandard.netPriceOf(inputPrice),
-                priceIncludesTax: false,
-                priceWithTax: taxRateDefaultStandard.netPriceOf(inputPrice),
-                priceWithoutTax: taxRateDefaultStandard.netPriceOf(inputPrice),
-            });
-        });
-    });
-});

+ 0 - 47
packages/core/src/service/helpers/tax-calculator/tax-calculator.ts

@@ -1,47 +0,0 @@
-import { Injectable } from '@nestjs/common';
-
-import { RequestContext } from '../../../api/common/request-context';
-import { idsAreEqual } from '../../../common/utils';
-import { ConfigService } from '../../../config/config.service';
-import { Channel } from '../../../entity/channel/channel.entity';
-import { TaxCategory } from '../../../entity/tax-category/tax-category.entity';
-import { TaxRate } from '../../../entity/tax-rate/tax-rate.entity';
-import { Zone } from '../../../entity/zone/zone.entity';
-import { TaxRateService } from '../../services/tax-rate.service';
-
-/**
- * @description
- * The result of the {@link TaxCalculationStrategy}.calculate() method.
- *
- * @docsCategory tax
- * @docsPage Tax Types
- */
-export interface TaxCalculationResult {
-    price: number;
-    priceIncludesTax: boolean;
-}
-
-@Injectable()
-export class TaxCalculator {
-    constructor(private configService: ConfigService, private taxRateService: TaxRateService) {}
-
-    /**
-     * Given a price and TaxCategory, this method calculates the applicable tax rate and returns the adjusted
-     * price along with other contextual information.
-     */
-    calculate(
-        inputPrice: number,
-        taxCategory: TaxCategory,
-        activeTaxZone: Zone,
-        ctx: RequestContext,
-    ): TaxCalculationResult {
-        const { taxCalculationStrategy } = this.configService.taxOptions;
-        return taxCalculationStrategy.calculate({
-            activeTaxZone,
-            taxRateService: this.taxRateService,
-            taxCategory,
-            ctx,
-            inputPrice,
-        });
-    }
-}

+ 0 - 2
packages/core/src/service/service.module.ts

@@ -23,7 +23,6 @@ import { RefundStateMachine } from './helpers/refund-state-machine/refund-state-
 import { ShippingCalculator } from './helpers/shipping-calculator/shipping-calculator';
 import { ShippingConfiguration } from './helpers/shipping-configuration/shipping-configuration';
 import { SlugValidator } from './helpers/slug-validator/slug-validator';
-import { TaxCalculator } from './helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from './helpers/translatable-saver/translatable-saver';
 import { VerificationTokenGenerator } from './helpers/verification-token-generator/verification-token-generator';
 import { InitializerService } from './initializer.service';
@@ -95,7 +94,6 @@ const services = [
 const helpers = [
     TranslatableSaver,
     PasswordCiper,
-    TaxCalculator,
     OrderCalculator,
     OrderStateMachine,
     FulfillmentStateMachine,

+ 1 - 1
packages/core/src/service/services/collection.service.ts

@@ -19,7 +19,7 @@ import { IllegalOperationError, UserInputError } from '../../common/error/errors
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
-import { CollectionFilter } from '../../config/collection/collection-filter';
+import { CollectionFilter } from '../../config/catalog/collection-filter';
 import { ConfigService } from '../../config/config.service';
 import { Logger } from '../../config/logger/vendure-logger';
 import { CollectionTranslation } from '../../entity/collection/collection-translation.entity';

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

@@ -104,7 +104,7 @@ export class OrderTestingService {
         shippingAddress: CreateAddressInput,
         lines: Array<{ productVariantId: ID; quantity: number }>,
     ): Promise<Order> {
-        const { priceCalculationStrategy } = this.configService.orderOptions;
+        const { orderItemPriceCalculationStrategy } = this.configService.orderOptions;
         const mockOrder = new Order({
             lines: [],
         });
@@ -124,7 +124,7 @@ export class OrderTestingService {
             });
             mockOrder.lines.push(orderLine);
 
-            const { price, priceIncludesTax } = await priceCalculationStrategy.calculateUnitPrice(
+            const { price, priceIncludesTax } = await orderItemPriceCalculationStrategy.calculateUnitPrice(
                 ctx,
                 productVariant,
                 orderLine.customFields || {},

+ 5 - 2
packages/core/src/service/services/order.service.ts

@@ -354,7 +354,7 @@ export class OrderService {
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         let correctedQuantity = quantity;
         let quantityWasAdjustedDown = false;
-        const { priceCalculationStrategy } = this.configService.orderOptions;
+        const { orderItemPriceCalculationStrategy } = this.configService.orderOptions;
         const order =
             orderIdOrOrder instanceof Order
                 ? orderIdOrOrder
@@ -389,7 +389,10 @@ export class OrderService {
                     orderLine.items = [];
                 }
                 const productVariant = orderLine.productVariant;
-                const { price, priceIncludesTax } = await priceCalculationStrategy.calculateUnitPrice(
+                const {
+                    price,
+                    priceIncludesTax,
+                } = await orderItemPriceCalculationStrategy.calculateUnitPrice(
                     ctx,
                     productVariant,
                     orderLine.customFields || {},

+ 5 - 6
packages/core/src/service/services/product-variant.service.ts

@@ -28,7 +28,6 @@ import { EventBus } from '../../event-bus/event-bus';
 import { ProductVariantChannelEvent } from '../../event-bus/events/product-variant-channel-event';
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { samplesEach } from '../helpers/utils/samples-each';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -52,7 +51,6 @@ export class ProductVariantService {
         private taxCategoryService: TaxCategoryService,
         private facetValueService: FacetValueService,
         private taxRateService: TaxRateService,
-        private taxCalculator: TaxCalculator,
         private assetService: AssetService,
         private zoneService: ZoneService,
         private translatableSaver: TranslatableSaver,
@@ -438,12 +436,13 @@ export class ProductVariantService {
             variant.taxCategory,
         );
 
-        const { price, priceIncludesTax } = this.taxCalculator.calculate(
-            channelPrice.price,
-            variant.taxCategory,
+        const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
+        const { price, priceIncludesTax } = productVariantPriceCalculationStrategy.calculate({
+            inputPrice: channelPrice.price,
+            taxCategory: variant.taxCategory,
             activeTaxZone,
             ctx,
-        );
+        });
 
         variant.listPrice = price;
         variant.listPriceIncludesTax = priceIncludesTax;

+ 93 - 0
packages/core/src/testing/order-test-utils.ts

@@ -1,9 +1,15 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 
+import { RequestContext } from '../api/common/request-context';
+import { Channel } from '../entity/channel/channel.entity';
 import { OrderItem } from '../entity/order-item/order-item.entity';
 import { OrderLine } from '../entity/order-line/order-line.entity';
 import { Order } from '../entity/order/order.entity';
 import { ProductVariant } from '../entity/product-variant/product-variant.entity';
+import { TaxCategory } from '../entity/tax-category/tax-category.entity';
+import { TaxRate } from '../entity/tax-rate/tax-rate.entity';
+import { Zone } from '../entity/zone/zone.entity';
 
 export type SimpleLine = { productVariantId: ID; quantity: number; lineId: ID };
 
@@ -31,3 +37,90 @@ export function parseLines(lines: OrderLine[]): SimpleLine[] {
         };
     });
 }
+
+export function createRequestContext(options: { pricesIncludeTax: boolean }): RequestContext {
+    const channel = new Channel({
+        defaultTaxZone: zoneDefault,
+        pricesIncludeTax: options.pricesIncludeTax,
+    });
+    const ctx = new RequestContext({
+        apiType: 'admin',
+        channel,
+        authorizedAsOwnerOnly: false,
+        languageCode: LanguageCode.en,
+        isAuthorized: true,
+        session: {} as any,
+    });
+    return ctx;
+}
+
+export const taxCategoryStandard = new TaxCategory({
+    id: 'taxCategoryStandard',
+    name: 'Standard Tax',
+});
+export const taxCategoryReduced = new TaxCategory({
+    id: 'taxCategoryReduced',
+    name: 'Reduced Tax',
+});
+export const zoneDefault = new Zone({
+    id: 'zoneDefault',
+    name: 'Default Zone',
+});
+export const zoneOther = new Zone({
+    id: 'zoneOther',
+    name: 'Other Zone',
+});
+export const zoneWithNoTaxRate = new Zone({
+    id: 'zoneWithNoTaxRate',
+    name: 'Zone for which no TaxRate is configured',
+});
+export const taxRateDefaultStandard = new TaxRate({
+    id: 'taxRateDefaultStandard',
+    name: 'Default Standard',
+    value: 20,
+    enabled: true,
+    zone: zoneDefault,
+    category: taxCategoryStandard,
+});
+export const taxRateDefaultReduced = new TaxRate({
+    id: 'taxRateDefaultReduced',
+    name: 'Default Reduced',
+    value: 10,
+    enabled: true,
+    zone: zoneDefault,
+    category: taxCategoryReduced,
+});
+export const taxRateOtherStandard = new TaxRate({
+    id: 'taxRateOtherStandard',
+    name: 'Other Standard',
+    value: 15,
+    enabled: true,
+    zone: zoneOther,
+    category: taxCategoryStandard,
+});
+export const taxRateOtherReduced = new TaxRate({
+    id: 'taxRateOtherReduced',
+    name: 'Other Reduced',
+    value: 5,
+    enabled: true,
+    zone: zoneOther,
+    category: taxCategoryReduced,
+});
+
+export class MockTaxRateService {
+    private activeTaxRates = [
+        taxRateDefaultStandard,
+        taxRateDefaultReduced,
+        taxRateOtherStandard,
+        taxRateOtherReduced,
+    ];
+
+    initTaxRates() {
+        /* noop */
+    }
+
+    getApplicableTaxRate(zone: Zone, taxCategory: TaxCategory): TaxRate {
+        const rate = this.activeTaxRates.find(r => r.test(zone, taxCategory));
+        return rate || taxRateDefaultStandard;
+    }
+}