Skip to content

Commit

Permalink
feat(core): Improve naming of price calculation strategies
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
michaelbromley committed Dec 9, 2020
1 parent 9544dd4 commit ccbebc9
Show file tree
Hide file tree
Showing 30 changed files with 461 additions and 471 deletions.
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ 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, {
customFields: {
OrderLine: [{ name: 'giftWrap', type: 'boolean' }],
},
orderOptions: {
priceCalculationStrategy: new TestPriceCalculationStrategy(),
orderItemPriceCalculationStrategy: new TestOrderItemPriceCalculationStrategy(),
},
plugins: [DefaultSearchPlugin],
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
];
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/common/types/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions packages/core/src/config/catalog/calculator-test-fixtures.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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,
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
Loading

0 comments on commit ccbebc9

Please sign in to comment.