Browse Source

refactor(core) Change the active tax rates to be cached per request

Fred Cox 5 years ago
parent
commit
5b7192489c
22 changed files with 182 additions and 133 deletions
  1. 9 0
      packages/core/src/cache/cache.module.ts
  2. 1 0
      packages/core/src/cache/index.ts
  3. 38 0
      packages/core/src/cache/request-context-cache.service.spec.ts
  4. 28 0
      packages/core/src/cache/request-context-cache.service.ts
  5. 20 20
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.spec.ts
  6. 4 3
      packages/core/src/config/catalog/default-product-variant-price-calculation-strategy.ts
  7. 1 1
      packages/core/src/config/catalog/product-variant-price-calculation-strategy.ts
  8. 1 1
      packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts
  9. 3 0
      packages/core/src/plugin/plugin-common.module.ts
  10. 0 22
      packages/core/src/service/controllers/tax-rate.controller.ts
  11. 0 2
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  12. 10 7
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  13. 1 1
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  14. 0 3
      packages/core/src/service/initializer.service.ts
  15. 2 6
      packages/core/src/service/service.module.ts
  16. 1 1
      packages/core/src/service/services/order-testing.service.ts
  17. 3 3
      packages/core/src/service/services/order.service.ts
  18. 37 27
      packages/core/src/service/services/product-variant.service.ts
  19. 15 21
      packages/core/src/service/services/tax-rate.service.ts
  20. 0 10
      packages/core/src/service/types/tax-rate-messages.ts
  21. 1 1
      packages/core/src/testing/order-test-utils.ts
  22. 7 4
      packages/elasticsearch-plugin/src/indexer.controller.ts

+ 9 - 0
packages/core/src/cache/cache.module.ts

@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+
+import { RequestContextCacheService } from './request-context-cache.service';
+
+@Module({
+    providers: [RequestContextCacheService],
+    exports: [RequestContextCacheService],
+})
+export class CacheModule {}

+ 1 - 0
packages/core/src/cache/index.ts

@@ -0,0 +1 @@
+export * from './request-context-cache.service';

+ 38 - 0
packages/core/src/cache/request-context-cache.service.spec.ts

@@ -0,0 +1,38 @@
+import { RequestContext } from '../api';
+
+import { RequestContextCacheService } from './request-context-cache.service';
+
+describe('Request context cache', () => {
+    let cache: RequestContextCacheService;
+    beforeEach(() => {
+        cache = new RequestContextCacheService();
+    });
+
+    it('stores and retrieves a multiple values', async () => {
+        const ctx = RequestContext.empty();
+
+        await cache.set(ctx, 'test', 1);
+        await cache.set(ctx, 'test2', 2);
+        expect(await cache.get(ctx, 'test')).toBe(1);
+        expect(await cache.get(ctx, 'test2')).toBe(2);
+    });
+
+    it('can use objects as keys', async () => {
+        const ctx = RequestContext.empty();
+
+        const x = {};
+        await cache.set(ctx, x, 1);
+        expect(await cache.get(ctx, x)).toBe(1);
+    });
+
+    it('uses separate stores per context', async () => {
+        const ctx = RequestContext.empty();
+        const ctx2 = RequestContext.empty();
+
+        await cache.set(ctx, 'test', 1);
+        await cache.set(ctx2, 'test', 2);
+
+        expect(await cache.get(ctx, 'test')).toBe(1);
+        expect(await cache.get(ctx2, 'test')).toBe(2);
+    });
+});

+ 28 - 0
packages/core/src/cache/request-context-cache.service.ts

@@ -0,0 +1,28 @@
+import { RequestContext } from '../api';
+
+export class RequestContextCacheService {
+    private caches = new WeakMap<RequestContext, Map<any, any>>();
+
+    async set(ctx: RequestContext, key: any, val: any): Promise<void> {
+        this.getContextCache(ctx).set(key, val);
+    }
+
+    async get(ctx: RequestContext, key: any, getDefault?: () => Promise<any>): Promise<any> {
+        const ctxCache = this.getContextCache(ctx);
+        let result = ctxCache.get(key);
+        if (!result && getDefault) {
+            result = await getDefault();
+            ctxCache.set(key, result);
+        }
+        return result;
+    }
+
+    private getContextCache(ctx: RequestContext): Map<any, any> {
+        let ctxCache = this.caches.get(ctx);
+        if (!ctxCache) {
+            ctxCache = new Map<any, any>();
+            this.caches.set(ctx, ctxCache);
+        }
+        return ctxCache;
+    }
+}

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

@@ -29,9 +29,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
     });
 
     describe('with prices which do not include tax', () => {
-        it('standard tax, default zone', () => {
+        it('standard tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneDefault,
@@ -44,9 +44,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('reduced tax, default zone', () => {
+        it('reduced tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneDefault,
@@ -59,9 +59,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('standard tax, other zone', () => {
+        it('standard tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneOther,
@@ -74,9 +74,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('reduced tax, other zone', () => {
+        it('reduced tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneOther,
@@ -89,9 +89,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('standard tax, unconfigured zone', () => {
+        it('standard tax, unconfigured zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: false });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneWithNoTaxRate,
@@ -106,9 +106,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
     });
 
     describe('with prices which include tax', () => {
-        it('standard tax, default zone', () => {
+        it('standard tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneDefault,
@@ -121,9 +121,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('reduced tax, default zone', () => {
+        it('reduced tax, default zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneDefault,
@@ -136,9 +136,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('standard tax, other zone', () => {
+        it('standard tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneOther,
@@ -151,9 +151,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('reduced tax, other zone', () => {
+        it('reduced tax, other zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryReduced,
                 activeTaxZone: zoneOther,
@@ -166,9 +166,9 @@ describe('DefaultProductVariantPriceCalculationStrategy', () => {
             });
         });
 
-        it('standard tax, unconfigured zone', () => {
+        it('standard tax, unconfigured zone', async () => {
             const ctx = createRequestContext({ pricesIncludeTax: true });
-            const result = strategy.calculate({
+            const result = await strategy.calculate({
                 inputPrice,
                 taxCategory: taxCategoryStandard,
                 activeTaxZone: zoneWithNoTaxRate,

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

@@ -21,18 +21,19 @@ export class DefaultProductVariantPriceCalculationStrategy implements ProductVar
         this.taxRateService = injector.get(TaxRateService);
     }
 
-    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult {
+    async calculate(args: ProductVariantPriceCalculationArgs): Promise<PriceCalculationResult> {
         const { inputPrice, activeTaxZone, ctx, taxCategory } = args;
         let price = inputPrice;
         let priceIncludesTax = false;
-        const taxRate = this.taxRateService.getApplicableTaxRate(activeTaxZone, taxCategory);
+        const taxRate = await this.taxRateService.getApplicableTaxRate(ctx, 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(
+                const taxRateForDefaultZone = await this.taxRateService.getApplicableTaxRate(
+                    ctx,
                     ctx.channel.defaultTaxZone,
                     taxCategory,
                 );

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

@@ -12,7 +12,7 @@ import { TaxRateService } from '../../service/services/tax-rate.service';
  * @docsPage ProductVariantPriceCalculationStrategy
  */
 export interface ProductVariantPriceCalculationStrategy extends InjectableStrategy {
-    calculate(args: ProductVariantPriceCalculationArgs): PriceCalculationResult;
+    calculate(args: ProductVariantPriceCalculationArgs): Promise<PriceCalculationResult>;
 }
 
 /**

+ 1 - 1
packages/core/src/plugin/default-search-plugin/indexer/indexer.controller.ts

@@ -318,7 +318,7 @@ export class IndexerController {
                         isAuthorized: true,
                         session: {} as any,
                     });
-                    this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+                    await this.productVariantService.applyChannelPriceAndTax(variant, ctx);
                     items.push(
                         new SearchIndexItem({
                             channelId: channel.id,

+ 3 - 0
packages/core/src/plugin/plugin-common.module.ts

@@ -1,5 +1,6 @@
 import { Module } from '@nestjs/common';
 
+import { CacheModule } from '../cache/cache.module';
 import { ConfigModule } from '../config/config.module';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { HealthCheckModule } from '../health-check/health-check.module';
@@ -32,6 +33,7 @@ import { WorkerServiceModule } from '../worker/worker-service.module';
         WorkerServiceModule,
         JobQueueModule,
         HealthCheckModule,
+        CacheModule,
     ],
     exports: [
         EventBusModule,
@@ -40,6 +42,7 @@ import { WorkerServiceModule } from '../worker/worker-service.module';
         WorkerServiceModule,
         JobQueueModule,
         HealthCheckModule,
+        CacheModule,
     ],
 })
 export class PluginCommonModule {}

+ 0 - 22
packages/core/src/service/controllers/tax-rate.controller.ts

@@ -1,22 +0,0 @@
-import { Controller } from '@nestjs/common';
-import { MessagePattern } from '@nestjs/microservices';
-import { from, Observable } from 'rxjs';
-
-import { RequestContext } from '../../api/common/request-context';
-import { TaxRateService } from '../services/tax-rate.service';
-import { TransactionalConnection } from '../transaction/transactional-connection';
-import { TaxRateUpdatedMessage } from '../types/tax-rate-messages';
-
-@Controller()
-export class TaxRateController {
-    constructor(private connection: TransactionalConnection, private taxRateService: TaxRateService) {}
-
-    /**
-     * When a TaxRate is updated on the main process, this will update the activeTaxRates
-     * cache on the worker.
-     */
-    @MessagePattern(TaxRateUpdatedMessage.pattern)
-    taxRateUpdated(): Observable<TaxRateUpdatedMessage['response']> {
-        return from(this.taxRateService.updateActiveTaxRates(RequestContext.empty()).then(() => true));
-    }
-}

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

@@ -27,7 +27,6 @@ import {
     taxCategoryStandard,
     taxCategoryZero,
 } 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';
@@ -1559,7 +1558,6 @@ function createTestModule() {
             { provide: ListQueryBuilder, useValue: {} },
             { provide: ConfigService, useClass: MockConfigService },
             { provide: EventBus, useValue: { publish: () => ({}) } },
-            { provide: WorkerService, useValue: { send: () => ({}) } },
             { provide: ZoneService, useValue: { findAll: () => [] } },
         ],
     }).compile();

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

@@ -59,7 +59,7 @@ export class OrderCalculator {
                 order,
                 updatedOrderLine,
                 activeTaxZone,
-                this.createTaxRateGetter(activeTaxZone),
+                this.createTaxRateGetter(ctx, activeTaxZone),
             );
             updatedOrderLine.activeItems.forEach(item => updatedOrderItems.add(item));
         }
@@ -93,7 +93,7 @@ export class OrderCalculator {
      * Applies the correct TaxRate to each OrderItem in the order.
      */
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
-        const getTaxRate = this.createTaxRateGetter(activeZone);
+        const getTaxRate = this.createTaxRateGetter(ctx, activeZone);
         for (const line of order.lines) {
             await this.applyTaxesToOrderLine(ctx, order, line, activeZone, getTaxRate);
         }
@@ -108,9 +108,9 @@ export class OrderCalculator {
         order: Order,
         line: OrderLine,
         activeZone: Zone,
-        getTaxRate: (taxCategory: TaxCategory) => TaxRate,
+        getTaxRate: (taxCategory: TaxCategory) => Promise<TaxRate>,
     ) {
-        const applicableTaxRate = getTaxRate(line.taxCategory);
+        const applicableTaxRate = await getTaxRate(line.taxCategory);
         const { taxLineCalculationStrategy } = this.configService.taxOptions;
         for (const item of line.activeItems) {
             item.taxLines = await taxLineCalculationStrategy.calculate({
@@ -127,15 +127,18 @@ export class OrderCalculator {
      * Returns a memoized function for performing an efficient
      * lookup of the correct TaxRate for a given TaxCategory.
      */
-    private createTaxRateGetter(activeZone: Zone): (taxCategory: TaxCategory) => TaxRate {
+    private createTaxRateGetter(
+        ctx: RequestContext,
+        activeZone: Zone,
+    ): (taxCategory: TaxCategory) => Promise<TaxRate> {
         const taxRateCache = new Map<TaxCategory, TaxRate>();
 
-        return (taxCategory: TaxCategory): TaxRate => {
+        return async (taxCategory: TaxCategory): Promise<TaxRate> => {
             const cached = taxRateCache.get(taxCategory);
             if (cached) {
                 return cached;
             }
-            const rate = this.taxRateService.getApplicableTaxRate(activeZone, taxCategory);
+            const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategory);
             taxRateCache.set(taxCategory, rate);
             return rate;
         };

+ 1 - 1
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -125,7 +125,7 @@ export class OrderModifier {
             ],
         });
         lineWithRelations.productVariant = translateDeep(
-            this.productVariantService.applyChannelPriceAndTax(lineWithRelations.productVariant, ctx),
+            await this.productVariantService.applyChannelPriceAndTax(lineWithRelations.productVariant, ctx),
             ctx.languageCode,
         );
         order.lines.push(lineWithRelations);

+ 0 - 3
packages/core/src/service/initializer.service.ts

@@ -5,7 +5,6 @@ import { ChannelService } from './services/channel.service';
 import { GlobalSettingsService } from './services/global-settings.service';
 import { RoleService } from './services/role.service';
 import { ShippingMethodService } from './services/shipping-method.service';
-import { TaxRateService } from './services/tax-rate.service';
 
 /**
  * Only used internally to run the various service init methods in the correct
@@ -17,7 +16,6 @@ export class InitializerService {
         private channelService: ChannelService,
         private roleService: RoleService,
         private administratorService: AdministratorService,
-        private taxRateService: TaxRateService,
         private shippingMethodService: ShippingMethodService,
         private globalSettingsService: GlobalSettingsService,
     ) {}
@@ -33,7 +31,6 @@ export class InitializerService {
         await this.channelService.initChannels();
         await this.roleService.initRoles();
         await this.administratorService.initAdministrators();
-        await this.taxRateService.initTaxRates();
         await this.shippingMethodService.initShippingMethods();
     }
 }

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

@@ -2,14 +2,13 @@ import { DynamicModule, Module } from '@nestjs/common';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { ConnectionOptions } from 'typeorm';
 
+import { CacheModule } from '../cache/cache.module';
 import { ConfigModule } from '../config/config.module';
 import { ConfigService } from '../config/config.service';
 import { TypeOrmLogger } from '../config/logger/typeorm-logger';
 import { EventBusModule } from '../event-bus/event-bus.module';
 import { JobQueueModule } from '../job-queue/job-queue.module';
-import { WorkerServiceModule } from '../worker/worker-service.module';
 
-import { TaxRateController } from './controllers/tax-rate.controller';
 import { ConfigArgService } from './helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from './helpers/custom-field-relation/custom-field-relation.service';
 import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
@@ -116,8 +115,6 @@ const helpers = [
     CustomFieldRelationService,
 ];
 
-const workerControllers = [TaxRateController];
-
 let defaultTypeOrmModule: DynamicModule;
 let workerTypeOrmModule: DynamicModule;
 
@@ -127,7 +124,7 @@ let workerTypeOrmModule: DynamicModule;
  * only run a single time.
  */
 @Module({
-    imports: [ConfigModule, EventBusModule, WorkerServiceModule, JobQueueModule],
+    imports: [ConfigModule, EventBusModule, CacheModule, JobQueueModule],
     providers: [...services, ...helpers, InitializerService],
     exports: [...services, ...helpers],
 })
@@ -195,7 +192,6 @@ export class ServiceModule {
         return {
             module: ServiceModule,
             imports: [workerTypeOrmModule, ConfigModule],
-            controllers: workerControllers,
         };
     }
 

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

@@ -118,7 +118,7 @@ export class OrderTestingService {
                 line.productVariantId,
                 { relations: ['taxCategory'] },
             );
-            this.productVariantService.applyChannelPriceAndTax(productVariant, ctx);
+            await this.productVariantService.applyChannelPriceAndTax(productVariant, ctx);
             const orderLine = new OrderLine({
                 productVariant,
                 items: [],

+ 3 - 3
packages/core/src/service/services/order.service.ts

@@ -204,12 +204,12 @@ export class OrderService {
             .addOrderBy('items.createdAt', 'ASC')
             .getOne();
         if (order) {
-            order.lines.forEach(line => {
+            for (const line of order.lines) {
                 line.productVariant = translateDeep(
-                    this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
+                    await this.productVariantService.applyChannelPriceAndTax(line.productVariant, ctx),
                     ctx.languageCode,
                 );
-            });
+            }
             return order;
         }
     }

+ 37 - 27
packages/core/src/service/services/product-variant.service.ts

@@ -78,8 +78,10 @@ export class ProductVariantService {
             })
             .getManyAndCount()
             .then(async ([variants, totalItems]) => {
-                const items = variants.map(variant =>
-                    translateDeep(this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode),
+                const items = await Promise.all(
+                    variants.map(async variant =>
+                        translateDeep(await this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode),
+                    ),
                 );
                 return {
                     items,
@@ -92,9 +94,9 @@ export class ProductVariantService {
         const relations = ['product', 'product.featuredAsset', 'taxCategory'];
         return this.connection
             .findOneInChannel(ctx, ProductVariant, productVariantId, ctx.channelId, { relations })
-            .then(result => {
+            .then(async result => {
                 if (result) {
-                    return translateDeep(this.applyChannelPriceAndTax(result, ctx), ctx.languageCode, [
+                    return translateDeep(await this.applyChannelPriceAndTax(result, ctx), ctx.languageCode, [
                         'product',
                     ]);
                 }
@@ -114,12 +116,14 @@ export class ProductVariantService {
                 ],
             })
             .then(variants => {
-                return variants.map(variant =>
-                    translateDeep(this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode, [
-                        'options',
-                        'facetValues',
-                        ['facetValues', 'facet'],
-                    ]),
+                return Promise.all(
+                    variants.map(async variant =>
+                        translateDeep(await this.applyChannelPriceAndTax(variant, ctx), ctx.languageCode, [
+                            'options',
+                            'facetValues',
+                            ['facetValues', 'facet'],
+                        ]),
+                    ),
                 );
             });
     }
@@ -147,15 +151,18 @@ export class ProductVariantService {
             .andWhere('productVariant.deletedAt IS NULL')
             .orderBy('productVariant.id', 'ASC')
             .getMany()
-            .then(variants =>
-                variants.map(variant => {
-                    const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);
-                    return translateDeep(variantWithPrices, ctx.languageCode, [
-                        'options',
-                        'facetValues',
-                        ['facetValues', 'facet'],
-                    ]);
-                }),
+            .then(
+                async variants =>
+                    await Promise.all(
+                        variants.map(async variant => {
+                            const variantWithPrices = await this.applyChannelPriceAndTax(variant, ctx);
+                            return translateDeep(variantWithPrices, ctx.languageCode, [
+                                'options',
+                                'facetValues',
+                                ['facetValues', 'facet'],
+                            ]);
+                        }),
+                    ),
             );
     }
 
@@ -180,10 +187,12 @@ export class ProductVariantService {
         }
 
         return qb.getManyAndCount().then(async ([variants, totalItems]) => {
-            const items = variants.map(variant => {
-                const variantWithPrices = this.applyChannelPriceAndTax(variant, ctx);
-                return translateDeep(variantWithPrices, ctx.languageCode);
-            });
+            const items = await Promise.all(
+                variants.map(async variant => {
+                    const variantWithPrices = await this.applyChannelPriceAndTax(variant, ctx);
+                    return translateDeep(variantWithPrices, ctx.languageCode);
+                }),
+            );
             return {
                 items,
                 totalItems,
@@ -484,7 +493,7 @@ export class ProductVariantService {
     /**
      * Populates the `price` field with the price for the specified channel.
      */
-    applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): ProductVariant {
+    async applyChannelPriceAndTax(variant: ProductVariant, ctx: RequestContext): Promise<ProductVariant> {
         const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
         if (!channelPrice) {
             throw new InternalServerError(`error.no-price-found-for-channel`, {
@@ -498,13 +507,14 @@ export class ProductVariantService {
         if (!activeTaxZone) {
             throw new InternalServerError(`error.no-active-tax-zone`);
         }
-        const applicableTaxRate = this.taxRateService.getApplicableTaxRate(
+        const applicableTaxRate = await this.taxRateService.getApplicableTaxRate(
+            ctx,
             activeTaxZone,
             variant.taxCategory,
         );
 
         const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
-        const { price, priceIncludesTax } = productVariantPriceCalculationStrategy.calculate({
+        const { price, priceIncludesTax } = await productVariantPriceCalculationStrategy.calculate({
             inputPrice: channelPrice.price,
             taxCategory: variant.taxCategory,
             activeTaxZone,
@@ -535,7 +545,7 @@ export class ProductVariantService {
             .findByIds(input.productVariantIds, { relations: ['taxCategory', 'assets'] });
         const priceFactor = input.priceFactor != null ? input.priceFactor : 1;
         for (const variant of variants) {
-            this.applyChannelPriceAndTax(variant, ctx);
+            await this.applyChannelPriceAndTax(variant, ctx);
             await this.channelService.assignToChannels(ctx, Product, variant.productId, [input.channelId]);
             await this.channelService.assignToChannels(ctx, ProductVariant, variant.id, [input.channelId]);
             await this.createOrUpdateProductVariantPrice(

+ 15 - 21
packages/core/src/service/services/tax-rate.service.ts

@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
 import {
     CreateTaxRateInput,
     DeletionResponse,
@@ -8,6 +8,7 @@ import {
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../api/common/request-context';
+import { RequestContextCacheService } from '../../cache';
 import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
@@ -17,19 +18,14 @@ import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus/event-bus';
 import { TaxRateModificationEvent } from '../../event-bus/events/tax-rate-modification-event';
-import { WorkerService } from '../../worker/worker.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { TransactionalConnection } from '../transaction/transactional-connection';
-import { TaxRateUpdatedMessage } from '../types/tax-rate-messages';
+
+const activeTaxRatesKey = 'active-tax-rates';
 
 @Injectable()
 export class TaxRateService {
-    /**
-     * We cache all active TaxRates to avoid hitting the DB many times
-     * per request.
-     */
-    private activeTaxRates: TaxRate[] = [];
     private readonly defaultTaxRate = new TaxRate({
         value: 0,
         enabled: true,
@@ -41,13 +37,9 @@ export class TaxRateService {
         private connection: TransactionalConnection,
         private eventBus: EventBus,
         private listQueryBuilder: ListQueryBuilder,
-        private workerService: WorkerService,
+        private cacheService: RequestContextCacheService,
     ) {}
 
-    async initTaxRates() {
-        return this.updateActiveTaxRates(RequestContext.empty());
-    }
-
     findAll(ctx: RequestContext, options?: ListQueryOptions<TaxRate>): Promise<PaginatedList<TaxRate>> {
         return this.listQueryBuilder
             .build(TaxRate, options, { relations: ['category', 'zone', 'customerGroup'], ctx })
@@ -77,7 +69,6 @@ export class TaxRateService {
         }
         const newTaxRate = await this.connection.getRepository(ctx, TaxRate).save(taxRate);
         await this.updateActiveTaxRates(ctx);
-        await this.workerService.send(new TaxRateUpdatedMessage(newTaxRate.id)).toPromise();
         this.eventBus.publish(new TaxRateModificationEvent(ctx, newTaxRate));
         return assertFound(this.findOne(ctx, newTaxRate.id));
     }
@@ -111,7 +102,6 @@ export class TaxRateService {
         // Commit the transaction so that the worker process can access the updated
         // TaxRate when updating its own tax rate cache.
         await this.connection.commitOpenTransaction(ctx);
-        await this.workerService.send(new TaxRateUpdatedMessage(updatedTaxRate.id)).toPromise();
 
         this.eventBus.publish(new TaxRateModificationEvent(ctx, updatedTaxRate));
         return assertFound(this.findOne(ctx, taxRate.id));
@@ -132,17 +122,21 @@ export class TaxRateService {
         }
     }
 
-    getActiveTaxRates(): TaxRate[] {
-        return this.activeTaxRates;
+    async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise<TaxRate> {
+        const rate = (await this.getActiveTaxRates(ctx)).find(r => r.test(zone, taxCategory));
+        return rate || this.defaultTaxRate;
     }
 
-    getApplicableTaxRate(zone: Zone, taxCategory: TaxCategory): TaxRate {
-        const rate = this.getActiveTaxRates().find(r => r.test(zone, taxCategory));
-        return rate || this.defaultTaxRate;
+    async getActiveTaxRates(ctx: RequestContext): Promise<TaxRate[]> {
+        return this.cacheService.get(ctx, activeTaxRatesKey, () => this.findActiveTaxRates(ctx));
     }
 
     async updateActiveTaxRates(ctx: RequestContext) {
-        this.activeTaxRates = await this.connection.getRepository(ctx, TaxRate).find({
+        await this.cacheService.set(ctx, activeTaxRatesKey, await this.findActiveTaxRates(ctx));
+    }
+
+    private async findActiveTaxRates(ctx: RequestContext): Promise<TaxRate[]> {
+        return await this.connection.getRepository(ctx, TaxRate).find({
             relations: ['category', 'zone', 'customerGroup'],
             where: {
                 enabled: true,

+ 0 - 10
packages/core/src/service/types/tax-rate-messages.ts

@@ -1,10 +0,0 @@
-import { ID } from '@vendure/common/lib/shared-types';
-
-import { WorkerMessage } from '../../worker/types';
-
-/**
- * A message sent to the Worker whenever a TaxRate is updated.
- */
-export class TaxRateUpdatedMessage extends WorkerMessage<ID, boolean> {
-    static readonly pattern = 'TaxRateUpdated';
-}

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

@@ -124,7 +124,7 @@ export class MockTaxRateService {
         /* noop */
     }
 
-    getApplicableTaxRate(zone: Zone, taxCategory: TaxCategory): TaxRate {
+    async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise<TaxRate> {
         const rate = this.activeTaxRates.find(r => r.test(zone, taxCategory));
         return rate || taxRateDefaultStandard;
     }

+ 7 - 4
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -711,10 +711,13 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     /**
      * Given an array of ProductVariants, this method applies the correct taxes and translations.
      */
-    private hydrateVariants(ctx: RequestContext, variants: ProductVariant[]): ProductVariant[] {
-        return variants
-            .map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx))
-            .map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
+    private async hydrateVariants(
+        ctx: RequestContext,
+        variants: ProductVariant[],
+    ): Promise<ProductVariant[]> {
+        return (
+            await Promise.all(variants.map(v => this.productVariantService.applyChannelPriceAndTax(v, ctx)))
+        ).map(v => translateDeep(v, ctx.languageCode, ['product', 'collections']));
     }
 
     private createVariantIndexItem(