Skip to content

Commit

Permalink
feat(core): Implement testShippingMethod query
Browse files Browse the repository at this point in the history
Relates to #133
  • Loading branch information
michaelbromley committed Aug 2, 2019
1 parent 554357f commit a3a9931
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 55 deletions.
30 changes: 30 additions & 0 deletions packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2809,6 +2810,11 @@ export type QueryShippingMethodArgs = {
};


export type QueryTestShippingMethodArgs = {
input: TestShippingMethodInput
};


export type QueryTaxCategoryArgs = {
id: Scalars['ID']
};
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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']>,
Expand Down
29 changes: 29 additions & 0 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2711,6 +2712,10 @@ export type QueryShippingMethodArgs = {
id: Scalars['ID'];
};

export type QueryTestShippingMethodArgs = {
input: TestShippingMethodInput;
};

export type QueryTaxCategoryArgs = {
id: Scalars['ID'];
};
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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']>;
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/api/resolvers/admin/shipping-method.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 changes: 23 additions & 0 deletions packages/core/src/api/schema/admin-api/shipping-method.api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ type Query {
shippingMethod(id: ID!): ShippingMethod
shippingEligibilityCheckers: [ConfigurableOperation!]!
shippingCalculators: [ConfigurableOperation!]!
testShippingMethod(input: TestShippingMethodInput!): TestShippingMethodResult!
}

type Mutation {
Expand All @@ -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!
}
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions packages/core/src/service/service.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -61,6 +63,7 @@ const exportedProviders = [
HistoryService,
JobService,
OrderService,
OrderTestingService,
PaymentMethodService,
ProductOptionGroupService,
ProductOptionService,
Expand Down Expand Up @@ -101,6 +104,7 @@ let workerTypeOrmModule: DynamicModule;
AssetUpdater,
VerificationTokenGenerator,
RefundStateMachine,
ShippingConfiguration,
],
exports: exportedProviders,
})
Expand Down
89 changes: 89 additions & 0 deletions packages/core/src/service/services/order-testing.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading

0 comments on commit a3a9931

Please sign in to comment.