Explorar o código

refactor(core): Extract product variant price application into service

Michael Bromley %!s(int64=4) %!d(string=hai) anos
pai
achega
469e3f72d0

+ 3 - 5
packages/core/src/api/common/custom-field-relation-resolver.service.ts

@@ -8,8 +8,8 @@ import { RelationCustomFieldConfig } from '../../config/custom-field/custom-fiel
 import { TransactionalConnection } from '../../connection/transactional-connection';
 import { VendureEntity } from '../../entity/base/base.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
+import { ProductPriceApplicator } from '../../service/helpers/product-price-applicator/product-price-applicator';
 import { translateDeep } from '../../service/helpers/utils/translate-entity';
-import { ProductVariantService } from '../../service/services/product-variant.service';
 
 import { RequestContext } from './request-context';
 
@@ -25,7 +25,7 @@ export class CustomFieldRelationResolverService {
     constructor(
         private connection: TransactionalConnection,
         private configService: ConfigService,
-        private productVariantService: ProductVariantService,
+        private productPriceApplicator: ProductPriceApplicator,
     ) {}
     /**
      * @description
@@ -82,8 +82,6 @@ export class CustomFieldRelationResolverService {
             .of(variant)
             .loadOne();
         variant.taxCategory = taxCategory;
-        // We use the ModuleRef to resolve the ProductVariantService here to
-        // avoid a circular dependency in the Nest DI.
-        return this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+        return this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
     }
 }

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

@@ -13,10 +13,9 @@ import { ConfigService } from '../../../config/config.service';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { FacetValue } from '../../../entity/facet-value/facet-value.entity';
-import { ProductVariantTranslation } from '../../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { Product } from '../../../entity/product/product.entity';
-import { ProductVariantService } from '../../../service/services/product-variant.service';
+import { ProductPriceApplicator } from '../../../service/helpers/product-price-applicator/product-price-applicator';
 import { SearchIndexItem } from '../search-index-item.entity';
 import {
     ProductChannelMessageData,
@@ -53,7 +52,7 @@ export class IndexerController {
 
     constructor(
         private connection: TransactionalConnection,
-        private productVariantService: ProductVariantService,
+        private productPriceApplicator: ProductPriceApplicator,
         private configService: ConfigService,
     ) {}
 
@@ -334,7 +333,7 @@ export class IndexerController {
                         isAuthorized: true,
                         session: {} as any,
                     });
-                    await this.productVariantService.applyChannelPriceAndTax(variant, ctx);
+                    await this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx);
                     items.push(
                         new SearchIndexItem({
                             channelId: channel.id,

+ 71 - 0
packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts

@@ -0,0 +1,71 @@
+import { Injectable } from '@nestjs/common';
+
+import { RequestContext } from '../../../api/common/request-context';
+import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
+import { InternalServerError } from '../../../common/error/errors';
+import { idsAreEqual } from '../../../common/utils';
+import { ConfigService } from '../../../config/config.service';
+import { Order } from '../../../entity/order/order.entity';
+import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
+import { TaxRateService } from '../../services/tax-rate.service';
+import { ZoneService } from '../../services/zone.service';
+
+/**
+ * @description
+ * This helper is used to apply the correct price to a ProductVariant based on the current context
+ * including active Channel, any current Order, etc.
+ */
+@Injectable()
+export class ProductPriceApplicator {
+    constructor(
+        private configService: ConfigService,
+        private taxRateService: TaxRateService,
+        private zoneService: ZoneService,
+        private requestCache: RequestContextCacheService,
+    ) {}
+
+    /**
+     * @description
+     * Populates the `price` field with the price for the specified channel.
+     */
+    async applyChannelPriceAndTax(
+        variant: ProductVariant,
+        ctx: RequestContext,
+        order?: Order,
+    ): Promise<ProductVariant> {
+        const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
+        if (!channelPrice) {
+            throw new InternalServerError(`error.no-price-found-for-channel`, {
+                variantId: variant.id,
+                channel: ctx.channel.code,
+            });
+        }
+        const { taxZoneStrategy } = this.configService.taxOptions;
+        const zones = await this.requestCache.get(ctx, 'allZones', () => this.zoneService.findAll(ctx));
+        const activeTaxZone = await this.requestCache.get(ctx, 'activeTaxZone', () =>
+            taxZoneStrategy.determineTaxZone(ctx, zones, ctx.channel, order),
+        );
+        if (!activeTaxZone) {
+            throw new InternalServerError(`error.no-active-tax-zone`);
+        }
+        const applicableTaxRate = await this.requestCache.get(
+            ctx,
+            `applicableTaxRate-${activeTaxZone.id}-${variant.taxCategory.id}`,
+            () => this.taxRateService.getApplicableTaxRate(ctx, activeTaxZone, variant.taxCategory),
+        );
+
+        const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
+        const { price, priceIncludesTax } = await productVariantPriceCalculationStrategy.calculate({
+            inputPrice: channelPrice.price,
+            taxCategory: variant.taxCategory,
+            activeTaxZone,
+            ctx,
+        });
+
+        variant.listPrice = price;
+        variant.listPriceIncludesTax = priceIncludesTax;
+        variant.taxRateApplied = applicableTaxRate;
+        variant.currencyCode = ctx.channel.currencyCode;
+        return variant;
+    }
+}

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

@@ -11,6 +11,7 @@ export * from './helpers/order-modifier/order-modifier';
 export * from './helpers/order-state-machine/order-state';
 export * from './helpers/password-cipher/password-cipher';
 export * from './helpers/payment-state-machine/payment-state';
+export * from './helpers/product-price-applicator/product-price-applicator';
 export * from './helpers/translatable-saver/translatable-saver';
 export * from './helpers/utils/patch-entity';
 export * from './helpers/utils/translate-entity';

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

@@ -19,6 +19,7 @@ import { OrderModifier } from './helpers/order-modifier/order-modifier';
 import { OrderStateMachine } from './helpers/order-state-machine/order-state-machine';
 import { PasswordCipher } from './helpers/password-cipher/password-cipher';
 import { PaymentStateMachine } from './helpers/payment-state-machine/payment-state-machine';
+import { ProductPriceApplicator } from './helpers/product-price-applicator/product-price-applicator';
 import { RefundStateMachine } from './helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from './helpers/shipping-calculator/shipping-calculator';
 import { SlugValidator } from './helpers/slug-validator/slug-validator';
@@ -112,6 +113,7 @@ const helpers = [
     CustomFieldRelationService,
     LocaleStringHydrator,
     ActiveOrderService,
+    ProductPriceApplicator,
 ];
 
 /**

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

@@ -21,11 +21,10 @@ import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
 import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
+import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
-import { ProductVariantService } from './product-variant.service';
-
 /**
  * This service is responsible for creating temporary mock Orders against which tests can be run, such as
  * testing a ShippingMethod or Promotion.
@@ -38,7 +37,7 @@ export class OrderTestingService {
         private shippingCalculator: ShippingCalculator,
         private configArgService: ConfigArgService,
         private configService: ConfigService,
-        private productVariantService: ProductVariantService,
+        private productPriceApplicator: ProductPriceApplicator,
     ) {}
 
     /**
@@ -119,7 +118,7 @@ export class OrderTestingService {
                 line.productVariantId,
                 { relations: ['taxCategory'] },
             );
-            await this.productVariantService.applyChannelPriceAndTax(productVariant, ctx, mockOrder);
+            await this.productPriceApplicator.applyChannelPriceAndTax(productVariant, ctx, mockOrder);
             const orderLine = new OrderLine({
                 productVariant,
                 items: [],

+ 3 - 39
packages/core/src/service/services/product-variant.service.ts

@@ -10,7 +10,6 @@ import {
     UpdateProductVariantInput,
 } from '@vendure/common/lib/generated-types';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
-import { FindOptionsUtils } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
 import { RequestContextCacheService } from '../../cache/request-context-cache.service';
@@ -38,6 +37,7 @@ import { ProductVariantChannelEvent } from '../../event-bus/events/product-varia
 import { ProductVariantEvent } from '../../event-bus/events/product-variant-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { samplesEach } from '../helpers/utils/samples-each';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -49,8 +49,6 @@ import { GlobalSettingsService } from './global-settings.service';
 import { RoleService } from './role.service';
 import { StockMovementService } from './stock-movement.service';
 import { TaxCategoryService } from './tax-category.service';
-import { TaxRateService } from './tax-rate.service';
-import { ZoneService } from './zone.service';
 
 @Injectable()
 export class ProductVariantService {
@@ -59,9 +57,7 @@ export class ProductVariantService {
         private configService: ConfigService,
         private taxCategoryService: TaxCategoryService,
         private facetValueService: FacetValueService,
-        private taxRateService: TaxRateService,
         private assetService: AssetService,
-        private zoneService: ZoneService,
         private translatableSaver: TranslatableSaver,
         private eventBus: EventBus,
         private listQueryBuilder: ListQueryBuilder,
@@ -71,6 +67,7 @@ export class ProductVariantService {
         private roleService: RoleService,
         private customFieldRelationService: CustomFieldRelationService,
         private requestCache: RequestContextCacheService,
+        private productPriceApplicator: ProductPriceApplicator,
     ) {}
 
     async findAll(
@@ -577,40 +574,7 @@ export class ProductVariantService {
         ctx: RequestContext,
         order?: Order,
     ): Promise<ProductVariant> {
-        const channelPrice = variant.productVariantPrices.find(p => idsAreEqual(p.channelId, ctx.channelId));
-        if (!channelPrice) {
-            throw new InternalServerError(`error.no-price-found-for-channel`, {
-                variantId: variant.id,
-                channel: ctx.channel.code,
-            });
-        }
-        const { taxZoneStrategy } = this.configService.taxOptions;
-        const zones = await this.requestCache.get(ctx, 'allZones', () => this.zoneService.findAll(ctx));
-        const activeTaxZone = await this.requestCache.get(ctx, 'activeTaxZone', () =>
-            taxZoneStrategy.determineTaxZone(ctx, zones, ctx.channel, order),
-        );
-        if (!activeTaxZone) {
-            throw new InternalServerError(`error.no-active-tax-zone`);
-        }
-        const applicableTaxRate = await this.requestCache.get(
-            ctx,
-            `applicableTaxRate-${activeTaxZone.id}-${variant.taxCategory.id}`,
-            () => this.taxRateService.getApplicableTaxRate(ctx, activeTaxZone, variant.taxCategory),
-        );
-
-        const { productVariantPriceCalculationStrategy } = this.configService.catalogOptions;
-        const { price, priceIncludesTax } = await productVariantPriceCalculationStrategy.calculate({
-            inputPrice: channelPrice.price,
-            taxCategory: variant.taxCategory,
-            activeTaxZone,
-            ctx,
-        });
-
-        variant.listPrice = price;
-        variant.listPriceIncludesTax = priceIncludesTax;
-        variant.taxRateApplied = applicableTaxRate;
-        variant.currencyCode = ctx.channel.currencyCode;
-        return variant;
+        return this.productPriceApplicator.applyChannelPriceAndTax(variant, ctx, order);
     }
 
     async assignProductVariantsToChannel(

+ 3 - 3
packages/elasticsearch-plugin/src/indexer.controller.ts

@@ -13,8 +13,8 @@ import {
     LanguageCode,
     Logger,
     Product,
+    ProductPriceApplicator,
     ProductVariant,
-    ProductVariantService,
     RequestContext,
     TransactionalConnection,
     Translatable,
@@ -78,7 +78,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
     constructor(
         private connection: TransactionalConnection,
         @Inject(ELASTIC_SEARCH_OPTIONS) private options: Required<ElasticsearchOptions>,
-        private productVariantService: ProductVariantService,
+        private productPriceApplicator: ProductPriceApplicator,
         private configService: ConfigService,
     ) {}
 
@@ -482,7 +482,7 @@ export class ElasticsearchIndexerController implements OnModuleInit, OnModuleDes
                         v.channels.map(c => c.id).includes(channelCtx.channelId),
                     );
                     for (const variant of variantsInChannel) {
-                        await this.productVariantService.applyChannelPriceAndTax(variant, channelCtx);
+                        await this.productPriceApplicator.applyChannelPriceAndTax(variant, channelCtx);
                     }
                     for (const languageCode of uniqueLanguageVariants) {
                         if (variantsInChannel.length) {