order-testing.service.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import { Injectable } from '@nestjs/common';
  2. import {
  3. CreateAddressInput,
  4. ShippingMethodQuote,
  5. TestEligibleShippingMethodsInput,
  6. TestShippingMethodInput,
  7. TestShippingMethodQuote,
  8. TestShippingMethodResult,
  9. } from '@vendure/common/lib/generated-types';
  10. import { ID } from '@vendure/common/lib/shared-types';
  11. import { RequestContext } from '../../api/common/request-context';
  12. import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
  13. import { ConfigService } from '../../config/config.service';
  14. import { TransactionalConnection } from '../../connection/transactional-connection';
  15. import { OrderLine } from '../../entity/order-line/order-line.entity';
  16. import { Order } from '../../entity/order/order.entity';
  17. import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
  18. import { ShippingLine } from '../../entity/shipping-line/shipping-line.entity';
  19. import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
  20. import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
  21. import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
  22. import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
  23. import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
  24. import { TranslatorService } from '../helpers/translator/translator.service';
  25. /**
  26. * @description
  27. * This service is responsible for creating temporary mock Orders against which tests can be run, such as
  28. * testing a ShippingMethod or Promotion.
  29. *
  30. * @docsCategory services
  31. */
  32. @Injectable()
  33. export class OrderTestingService {
  34. constructor(
  35. private connection: TransactionalConnection,
  36. private orderCalculator: OrderCalculator,
  37. private shippingCalculator: ShippingCalculator,
  38. private configArgService: ConfigArgService,
  39. private configService: ConfigService,
  40. private productPriceApplicator: ProductPriceApplicator,
  41. private translator: TranslatorService,
  42. ) {}
  43. /**
  44. * @description
  45. * Runs a given ShippingMethod configuration against a mock Order to test for eligibility and resulting
  46. * price.
  47. */
  48. async testShippingMethod(
  49. ctx: RequestContext,
  50. input: TestShippingMethodInput,
  51. ): Promise<TestShippingMethodResult> {
  52. const shippingMethod = new ShippingMethod({
  53. checker: this.configArgService.parseInput('ShippingEligibilityChecker', input.checker),
  54. calculator: this.configArgService.parseInput('ShippingCalculator', input.calculator),
  55. });
  56. const mockOrder = await this.buildMockOrder(ctx, input.shippingAddress, input.lines);
  57. const eligible = await shippingMethod.test(ctx, mockOrder);
  58. const result = eligible ? await shippingMethod.apply(ctx, mockOrder) : undefined;
  59. let quote: TestShippingMethodQuote | undefined;
  60. if (result) {
  61. const { price, priceIncludesTax, taxRate, metadata } = result;
  62. quote = {
  63. price: priceIncludesTax ? netPriceOf(price, taxRate) : price,
  64. priceWithTax: priceIncludesTax ? price : grossPriceOf(price, taxRate),
  65. metadata,
  66. };
  67. }
  68. return {
  69. eligible,
  70. quote,
  71. };
  72. }
  73. /**
  74. * @description
  75. * Tests all available ShippingMethods against a mock Order and return those which are eligible. This
  76. * is intended to simulate a call to the `eligibleShippingMethods` query of the Shop API.
  77. */
  78. async testEligibleShippingMethods(
  79. ctx: RequestContext,
  80. input: TestEligibleShippingMethodsInput,
  81. ): Promise<ShippingMethodQuote[]> {
  82. const mockOrder = await this.buildMockOrder(ctx, input.shippingAddress, input.lines);
  83. const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, mockOrder);
  84. return eligibleMethods
  85. .map(result => {
  86. this.translator.translate(result.method, ctx);
  87. return result;
  88. })
  89. .map(result => {
  90. const { price, taxRate, priceIncludesTax, metadata } = result.result;
  91. return {
  92. id: result.method.id,
  93. price: priceIncludesTax ? netPriceOf(price, taxRate) : price,
  94. priceWithTax: priceIncludesTax ? price : grossPriceOf(price, taxRate),
  95. name: result.method.name,
  96. code: result.method.code,
  97. description: result.method.description,
  98. metadata: result.result.metadata,
  99. };
  100. });
  101. }
  102. private async buildMockOrder(
  103. ctx: RequestContext,
  104. shippingAddress: CreateAddressInput,
  105. lines: Array<{ productVariantId: ID; quantity: number }>,
  106. ): Promise<Order> {
  107. const { orderItemPriceCalculationStrategy } = this.configService.orderOptions;
  108. const mockOrder = new Order({
  109. lines: [],
  110. surcharges: [],
  111. modifications: [],
  112. });
  113. mockOrder.shippingAddress = shippingAddress;
  114. for (const line of lines) {
  115. const productVariant = await this.connection.getEntityOrThrow(
  116. ctx,
  117. ProductVariant,
  118. line.productVariantId,
  119. { relations: ['taxCategory'] },
  120. );
  121. await this.productPriceApplicator.applyChannelPriceAndTax(productVariant, ctx, mockOrder);
  122. const orderLine = new OrderLine({
  123. productVariant,
  124. adjustments: [],
  125. taxLines: [],
  126. quantity: line.quantity,
  127. taxCategory: productVariant.taxCategory,
  128. });
  129. mockOrder.lines.push(orderLine);
  130. const { price, priceIncludesTax } = await orderItemPriceCalculationStrategy.calculateUnitPrice(
  131. ctx,
  132. productVariant,
  133. orderLine.customFields || {},
  134. mockOrder,
  135. );
  136. const taxRate = productVariant.taxRateApplied;
  137. orderLine.listPrice = price;
  138. orderLine.listPriceIncludesTax = priceIncludesTax;
  139. }
  140. mockOrder.shippingLines = [
  141. new ShippingLine({
  142. listPrice: 0,
  143. listPriceIncludesTax: ctx.channel.pricesIncludeTax,
  144. taxLines: [],
  145. adjustments: [],
  146. }),
  147. ];
  148. await this.orderCalculator.applyPriceAdjustments(ctx, mockOrder, []);
  149. return mockOrder;
  150. }
  151. }