فهرست منبع

feat(core): Implement testShippingMethod query

Relates to #133
Michael Bromley 6 سال پیش
والد
کامیت
a3a9931ba5

+ 30 - 0
packages/common/src/generated-types.ts

@@ -2644,6 +2644,7 @@ export type Query = {
   shippingMethod?: Maybe<ShippingMethod>,
   shippingEligibilityCheckers: Array<ConfigurableOperation>,
   shippingCalculators: Array<ConfigurableOperation>,
+  testShippingMethod: TestShippingMethodResult,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
   taxRates: TaxRateList,
@@ -2809,6 +2810,11 @@ export type QueryShippingMethodArgs = {
 };
 
 
+export type QueryTestShippingMethodArgs = {
+  input: TestShippingMethodInput
+};
+
+
 export type QueryTaxCategoryArgs = {
   id: Scalars['ID']
 };
@@ -3025,6 +3031,12 @@ export type ShippingMethodSortParameter = {
   description?: Maybe<SortOrder>,
 };
 
+export type ShippingPrice = {
+  __typename?: 'ShippingPrice',
+  price: Scalars['Int'],
+  priceWithTax: Scalars['Int'],
+};
+
 /** The price value where the result has a single price */
 export type SinglePrice = {
   __typename?: 'SinglePrice',
@@ -3148,6 +3160,24 @@ export type TaxRateSortParameter = {
   value?: Maybe<SortOrder>,
 };
 
+export type TestShippingMethodInput = {
+  checker: ConfigurableOperationInput,
+  calculator: ConfigurableOperationInput,
+  shippingAddress: CreateAddressInput,
+  lines: Array<TestShippingMethodOrderLineInput>,
+};
+
+export type TestShippingMethodOrderLineInput = {
+  productVariantId: Scalars['ID'],
+  quantity: Scalars['Int'],
+};
+
+export type TestShippingMethodResult = {
+  __typename?: 'TestShippingMethodResult',
+  eligible: Scalars['Boolean'],
+  price?: Maybe<ShippingPrice>,
+};
+
 export type UpdateAddressInput = {
   id: Scalars['ID'],
   fullName?: Maybe<Scalars['String']>,

+ 29 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -2578,6 +2578,7 @@ export type Query = {
     shippingMethod?: Maybe<ShippingMethod>;
     shippingEligibilityCheckers: Array<ConfigurableOperation>;
     shippingCalculators: Array<ConfigurableOperation>;
+    testShippingMethod: TestShippingMethodResult;
     taxCategories: Array<TaxCategory>;
     taxCategory?: Maybe<TaxCategory>;
     taxRates: TaxRateList;
@@ -2711,6 +2712,10 @@ export type QueryShippingMethodArgs = {
     id: Scalars['ID'];
 };
 
+export type QueryTestShippingMethodArgs = {
+    input: TestShippingMethodInput;
+};
+
 export type QueryTaxCategoryArgs = {
     id: Scalars['ID'];
 };
@@ -2926,6 +2931,12 @@ export type ShippingMethodSortParameter = {
     description?: Maybe<SortOrder>;
 };
 
+export type ShippingPrice = {
+    __typename?: 'ShippingPrice';
+    price: Scalars['Int'];
+    priceWithTax: Scalars['Int'];
+};
+
 /** The price value where the result has a single price */
 export type SinglePrice = {
     __typename?: 'SinglePrice';
@@ -3050,6 +3061,24 @@ export type TaxRateSortParameter = {
     value?: Maybe<SortOrder>;
 };
 
+export type TestShippingMethodInput = {
+    checker: ConfigurableOperationInput;
+    calculator: ConfigurableOperationInput;
+    shippingAddress: CreateAddressInput;
+    lines: Array<TestShippingMethodOrderLineInput>;
+};
+
+export type TestShippingMethodOrderLineInput = {
+    productVariantId: Scalars['ID'];
+    quantity: Scalars['Int'];
+};
+
+export type TestShippingMethodResult = {
+    __typename?: 'TestShippingMethodResult';
+    eligible: Scalars['Boolean'];
+    price?: Maybe<ShippingPrice>;
+};
+
 export type UpdateAddressInput = {
     id: Scalars['ID'];
     fullName?: Maybe<Scalars['String']>;

+ 16 - 2
packages/core/src/api/resolvers/admin/shipping-method.resolver.ts

@@ -2,20 +2,27 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
     ConfigurableOperation,
     MutationCreateShippingMethodArgs,
+    MutationUpdateShippingMethodArgs,
     Permission,
     QueryShippingMethodArgs,
     QueryShippingMethodsArgs,
-    MutationUpdateShippingMethodArgs,
+    QueryTestShippingMethodArgs,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { ShippingMethod } from '../../../entity/shipping-method/shipping-method.entity';
+import { OrderTestingService } from '../../../service/services/order-testing.service';
 import { ShippingMethodService } from '../../../service/services/shipping-method.service';
+import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
 
 @Resolver('ShippingMethod')
 export class ShippingMethodResolver {
-    constructor(private shippingMethodService: ShippingMethodService) {}
+    constructor(
+        private shippingMethodService: ShippingMethodService,
+        private orderTestingService: OrderTestingService,
+    ) {}
 
     @Query()
     @Allow(Permission.ReadSettings)
@@ -54,4 +61,11 @@ export class ShippingMethodResolver {
         const { input } = args;
         return this.shippingMethodService.update(input);
     }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    testShippingMethod(@Ctx() ctx: RequestContext, @Args() args: QueryTestShippingMethodArgs) {
+        const { input } = args;
+        return this.orderTestingService.testShippingMethod(ctx, input);
+    }
 }

+ 23 - 0
packages/core/src/api/schema/admin-api/shipping-method.api.graphql

@@ -3,6 +3,7 @@ type Query {
     shippingMethod(id: ID!): ShippingMethod
     shippingEligibilityCheckers: [ConfigurableOperation!]!
     shippingCalculators: [ConfigurableOperation!]!
+    testShippingMethod(input: TestShippingMethodInput!): TestShippingMethodResult!
 }
 
 type Mutation {
@@ -29,3 +30,25 @@ input UpdateShippingMethodInput {
     checker: ConfigurableOperationInput
     calculator: ConfigurableOperationInput
 }
+
+input TestShippingMethodInput {
+    checker: ConfigurableOperationInput!
+    calculator: ConfigurableOperationInput!
+    shippingAddress: CreateAddressInput!
+    lines: [TestShippingMethodOrderLineInput!]!
+}
+
+input TestShippingMethodOrderLineInput {
+    productVariantId: ID!
+    quantity: Int!
+}
+
+type TestShippingMethodResult {
+    eligible: Boolean!
+    price: ShippingPrice
+}
+
+type ShippingPrice {
+    price: Int!
+    priceWithTax: Int!
+}

+ 65 - 0
packages/core/src/service/helpers/shipping-configuration/shipping-configuration.ts

@@ -0,0 +1,65 @@
+import { Injectable } from '@nestjs/common';
+import { ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
+
+import { ConfigurableOperation } from '../../../../../common/lib/generated-types';
+import { UserInputError } from '../../../common/error/errors';
+import { ConfigService } from '../../../config/config.service';
+import { ShippingCalculator } from '../../../config/shipping-method/shipping-calculator';
+import { ShippingEligibilityChecker } from '../../../config/shipping-method/shipping-eligibility-checker';
+
+/**
+ * This helper class provides methods relating to ShippingMethod configurable operations (eligibility checkers
+ * and calculators).
+ */
+@Injectable()
+export class ShippingConfiguration {
+    readonly shippingEligibilityCheckers: ShippingEligibilityChecker[];
+    readonly shippingCalculators: ShippingCalculator[];
+
+    constructor(private configService: ConfigService) {
+        this.shippingEligibilityCheckers =
+            this.configService.shippingOptions.shippingEligibilityCheckers || [];
+        this.shippingCalculators = this.configService.shippingOptions.shippingCalculators || [];
+    }
+
+    parseCheckerInput(input: ConfigurableOperationInput): ConfigurableOperation {
+        const checker = this.getChecker(input.code);
+        return this.parseOperationArgs(input, checker);
+    }
+
+    parseCalculatorInput(input: ConfigurableOperationInput): ConfigurableOperation {
+        const calculator = this.getCalculator(input.code);
+        return this.parseOperationArgs(input, calculator);
+    }
+
+    /**
+     * Converts the input values of the "create" and "update" mutations into the format expected by the ShippingMethod entity.
+     */
+    private parseOperationArgs(
+        input: ConfigurableOperationInput,
+        checkerOrCalculator: ShippingEligibilityChecker | ShippingCalculator,
+    ): ConfigurableOperation {
+        const output: ConfigurableOperation = {
+            code: input.code,
+            description: checkerOrCalculator.description,
+            args: input.arguments,
+        };
+        return output;
+    }
+
+    private getChecker(code: string): ShippingEligibilityChecker {
+        const match = this.shippingEligibilityCheckers.find(a => a.code === code);
+        if (!match) {
+            throw new UserInputError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
+        }
+        return match;
+    }
+
+    private getCalculator(code: string): ShippingCalculator {
+        const match = this.shippingCalculators.find(a => a.code === code);
+        if (!match) {
+            throw new UserInputError(`error.shipping-calculator-with-code-not-found`, { code });
+        }
+        return match;
+    }
+}

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

@@ -14,6 +14,7 @@ import { PasswordCiper } from './helpers/password-cipher/password-ciper';
 import { PaymentStateMachine } from './helpers/payment-state-machine/payment-state-machine';
 import { RefundStateMachine } from './helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from './helpers/shipping-calculator/shipping-calculator';
+import { ShippingConfiguration } from './helpers/shipping-configuration/shipping-configuration';
 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';
@@ -30,6 +31,7 @@ import { FacetService } from './services/facet.service';
 import { GlobalSettingsService } from './services/global-settings.service';
 import { HistoryService } from './services/history.service';
 import { JobService } from './services/job.service';
+import { OrderTestingService } from './services/order-testing.service';
 import { OrderService } from './services/order.service';
 import { PaymentMethodService } from './services/payment-method.service';
 import { ProductOptionGroupService } from './services/product-option-group.service';
@@ -61,6 +63,7 @@ const exportedProviders = [
     HistoryService,
     JobService,
     OrderService,
+    OrderTestingService,
     PaymentMethodService,
     ProductOptionGroupService,
     ProductOptionService,
@@ -101,6 +104,7 @@ let workerTypeOrmModule: DynamicModule;
         AssetUpdater,
         VerificationTokenGenerator,
         RefundStateMachine,
+        ShippingConfiguration,
     ],
     exports: exportedProviders,
 })

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

@@ -0,0 +1,89 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import {
+    CreateAddressInput,
+    TestShippingMethodInput,
+    TestShippingMethodResult,
+} from '@vendure/common/lib/generated-types';
+import { Connection } from 'typeorm';
+
+import { ID } from '../../../../common/lib/shared-types';
+import { RequestContext } from '../../api/common/request-context';
+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 { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
+import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
+import { ShippingConfiguration } from '../helpers/shipping-configuration/shipping-configuration';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+
+/**
+ * This service is responsible for creating temporary mock Orders against which tests can be run, such as
+ * testing a ShippingMethod or Promotion.
+ */
+@Injectable()
+export class OrderTestingService {
+    constructor(
+        @InjectConnection() private connection: Connection,
+        private orderCalculator: OrderCalculator,
+        private shippingConfiguration: ShippingConfiguration,
+    ) {}
+
+    /**
+     * Runs a given ShippingMethod configuration against a mock Order to test for eligibility and resulting
+     * price.
+     */
+    async testShippingMethod(
+        ctx: RequestContext,
+        input: TestShippingMethodInput,
+    ): Promise<TestShippingMethodResult> {
+        const shippingMethod = new ShippingMethod({
+            checker: this.shippingConfiguration.parseCheckerInput(input.checker),
+            calculator: this.shippingConfiguration.parseCalculatorInput(input.calculator),
+        });
+        const mockOrder = await this.buildMockOrder(ctx, input.shippingAddress, input.lines);
+        const eligible = await shippingMethod.test(mockOrder);
+        const price = eligible ? await shippingMethod.apply(mockOrder) : undefined;
+        return {
+            eligible,
+            price,
+        };
+    }
+    private async buildMockOrder(
+        ctx: RequestContext,
+        shippingAddress: CreateAddressInput,
+        lines: Array<{ productVariantId: ID; quantity: number }>,
+    ): Promise<Order> {
+        const mockOrder = new Order({
+            lines: [],
+        });
+        mockOrder.shippingAddress = shippingAddress;
+        for (const line of lines) {
+            const productVariant = await getEntityOrThrow(
+                this.connection,
+                ProductVariant,
+                line.productVariantId,
+                { relations: ['taxCategory'] },
+            );
+            const orderLine = new OrderLine({
+                productVariant,
+                items: [],
+                taxCategory: productVariant.taxCategory,
+            });
+            mockOrder.lines.push(orderLine);
+
+            for (let i = 0; i < line.quantity; i++) {
+                const orderItem = new OrderItem({
+                    unitPrice: productVariant.price,
+                    pendingAdjustments: [],
+                    unitPriceIncludesTax: productVariant.priceIncludesTax,
+                    taxRate: productVariant.priceIncludesTax ? productVariant.taxRateApplied.value : 0,
+                });
+                orderLine.items.push(orderItem);
+            }
+        }
+        await this.orderCalculator.applyPriceAdjustments(ctx, mockOrder, []);
+        return mockOrder;
+    }
+}

+ 10 - 52
packages/core/src/service/services/shipping-method.service.ts

@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import {
     ConfigurableOperation,
-    ConfigurableOperationInput,
     CreateShippingMethodInput,
     UpdateShippingMethodInput,
 } from '@vendure/common/lib/generated-types';
@@ -11,23 +10,20 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 
 import { configurableDefToOperation } from '../../common/configurable-operation';
-import { EntityNotFoundError, UserInputError } from '../../common/error/errors';
+import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { assertFound } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
-import { ShippingCalculator } from '../../config/shipping-method/shipping-calculator';
-import { ShippingEligibilityChecker } from '../../config/shipping-method/shipping-eligibility-checker';
 import { Channel } from '../../entity/channel/channel.entity';
 import { ShippingMethod } from '../../entity/shipping-method/shipping-method.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { ShippingConfiguration } from '../helpers/shipping-configuration/shipping-configuration';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { ChannelService } from './channel.service';
 
 @Injectable()
 export class ShippingMethodService {
-    shippingEligibilityCheckers: ShippingEligibilityChecker[];
-    shippingCalculators: ShippingCalculator[];
     private activeShippingMethods: ShippingMethod[];
 
     constructor(
@@ -35,11 +31,8 @@ export class ShippingMethodService {
         private configService: ConfigService,
         private listQueryBuilder: ListQueryBuilder,
         private channelService: ChannelService,
-    ) {
-        this.shippingEligibilityCheckers =
-            this.configService.shippingOptions.shippingEligibilityCheckers || [];
-        this.shippingCalculators = this.configService.shippingOptions.shippingCalculators || [];
-    }
+        private shippingConfiguration: ShippingConfiguration,
+    ) {}
 
     async initShippingMethods() {
         await this.updateActiveShippingMethods();
@@ -65,8 +58,8 @@ export class ShippingMethodService {
         const shippingMethod = new ShippingMethod({
             code: input.code,
             description: input.description,
-            checker: this.parseOperationArgs(input.checker, this.getChecker(input.checker.code)),
-            calculator: this.parseOperationArgs(input.calculator, this.getCalculator(input.calculator.code)),
+            checker: this.shippingConfiguration.parseCheckerInput(input.checker),
+            calculator: this.shippingConfiguration.parseCalculatorInput(input.calculator),
         });
         shippingMethod.channels = [this.channelService.getDefaultChannel()];
         const newShippingMethod = await this.connection.manager.save(shippingMethod);
@@ -81,15 +74,11 @@ export class ShippingMethodService {
         }
         const updatedShippingMethod = patchEntity(shippingMethod, omit(input, ['checker', 'calculator']));
         if (input.checker) {
-            updatedShippingMethod.checker = this.parseOperationArgs(
-                input.checker,
-                this.getChecker(input.checker.code),
-            );
+            updatedShippingMethod.checker = this.shippingConfiguration.parseCheckerInput(input.checker);
         }
         if (input.calculator) {
-            updatedShippingMethod.calculator = this.parseOperationArgs(
+            updatedShippingMethod.calculator = this.shippingConfiguration.parseCalculatorInput(
                 input.calculator,
-                this.getCalculator(input.calculator.code),
             );
         }
         await this.connection.manager.save(updatedShippingMethod);
@@ -98,48 +87,17 @@ export class ShippingMethodService {
     }
 
     getShippingEligibilityCheckers(): ConfigurableOperation[] {
-        return this.shippingEligibilityCheckers.map(configurableDefToOperation);
+        return this.shippingConfiguration.shippingEligibilityCheckers.map(configurableDefToOperation);
     }
 
     getShippingCalculators(): ConfigurableOperation[] {
-        return this.shippingCalculators.map(configurableDefToOperation);
+        return this.shippingConfiguration.shippingCalculators.map(configurableDefToOperation);
     }
 
     getActiveShippingMethods(channel: Channel): ShippingMethod[] {
         return this.activeShippingMethods.filter(sm => sm.channels.find(c => c.id === channel.id));
     }
 
-    /**
-     * Converts the input values of the "create" and "update" mutations into the format expected by the ShippingMethod entity.
-     */
-    private parseOperationArgs(
-        input: ConfigurableOperationInput,
-        checkerOrCalculator: ShippingEligibilityChecker | ShippingCalculator,
-    ): ConfigurableOperation {
-        const output: ConfigurableOperation = {
-            code: input.code,
-            description: checkerOrCalculator.description,
-            args: input.arguments,
-        };
-        return output;
-    }
-
-    private getChecker(code: string): ShippingEligibilityChecker {
-        const match = this.shippingEligibilityCheckers.find(a => a.code === code);
-        if (!match) {
-            throw new UserInputError(`error.shipping-eligibility-checker-with-code-not-found`, { code });
-        }
-        return match;
-    }
-
-    private getCalculator(code: string): ShippingCalculator {
-        const match = this.shippingCalculators.find(a => a.code === code);
-        if (!match) {
-            throw new UserInputError(`error.shipping-calculator-with-code-not-found`, { code });
-        }
-        return match;
-    }
-
     private async updateActiveShippingMethods() {
         this.activeShippingMethods = await this.connection.getRepository(ShippingMethod).find({
             relations: ['channels'],

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema-admin.json


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است