Skip to content

Commit

Permalink
feat(core): Implement configurable PriceCalculationStrategy
Browse files Browse the repository at this point in the history
Relates to  #237
  • Loading branch information
michaelbromley committed May 15, 2020
1 parent 3583089 commit 3e2cc2b
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 34 deletions.
3 changes: 2 additions & 1 deletion packages/core/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
} = this.configService.assetOptions;
const { taxCalculationStrategy, taxZoneStrategy } = this.configService.taxOptions;
const { jobQueueStrategy } = this.configService.jobQueueOptions;
const { mergeStrategy } = this.configService.orderOptions;
const { mergeStrategy, priceCalculationStrategy } = this.configService.orderOptions;
const { entityIdStrategy } = this.configService;
return [
assetNamingStrategy,
Expand All @@ -142,6 +142,7 @@ export class AppModule implements NestModule, OnApplicationBootstrap, OnApplicat
jobQueueStrategy,
mergeStrategy,
entityIdStrategy,
priceCalculationStrategy,
];
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { defaultCollectionFilters } from './collection/default-collection-filter
import { AutoIncrementIdStrategy } from './entity-id-strategy/auto-increment-id-strategy';
import { DefaultLogger } from './logger/default-logger';
import { TypeOrmLogger } from './logger/typeorm-logger';
import { DefaultPriceCalculationStrategy } from './order/default-price-calculation-strategy';
import { MergeOrdersStrategy } from './order/merge-orders-strategy';
import { UseGuestStrategy } from './order/use-guest-strategy';
import { defaultPromotionActions } from './promotion/default-promotion-actions';
Expand Down Expand Up @@ -83,6 +84,7 @@ export const defaultConfig: RuntimeVendureConfig = {
},
orderOptions: {
orderItemsLimit: 999,
priceCalculationStrategy: new DefaultPriceCalculationStrategy(),
mergeStrategy: new MergeOrdersStrategy(),
checkoutMergeStrategy: new UseGuestStrategy(),
process: {},
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './logger/noop-logger';
export * from './logger/vendure-logger';
export * from './merge-config';
export * from './order/order-merge-strategy';
export * from './order/price-calculation-strategy';
export * from './payment-method/example-payment-method-handler';
export * from './payment-method/payment-method-handler';
export * from './promotion/default-promotion-actions';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ProductVariant } from '../../entity/product-variant/product-variant.entity';

import { CalculatedPrice, PriceCalculationStrategy } from './price-calculation-strategy';

/**
* @description
* The default {@link PriceCalculationStrategy}, which simply passes through the price of
* the ProductVariant without performing any calculations
*/
export class DefaultPriceCalculationStrategy implements PriceCalculationStrategy {
calculateUnitPrice(productVariant: ProductVariant): CalculatedPrice | Promise<CalculatedPrice> {
return productVariant;
}
}
60 changes: 60 additions & 0 deletions packages/core/src/config/order/price-calculation-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { InjectableStrategy } from '../../common/types/injectable-strategy';
import { ProductVariant } from '../../entity/product-variant/product-variant.entity';

/**
* @description
* The result of the price calculation from the {@link PriceCalculationStrategy}.
*
* @docsCateogory Orders
*/
export type CalculatedPrice = {
price: number;
priceIncludesTax: boolean;
};

/**
* @description
* The PriceCalculationStrategy defines the price of an OrderItem when a ProductVariant gets added
* to an order via the `addItemToOrder` mutation. By default the {@link DefaultPriceCalculationStrategy}
* is used.
*
* ### PriceCalculationStrategy vs Promotions
* Both the PriceCalculationStrategy and Promotions can be used to alter the price paid for a product.
*
* Use PriceCalculationStrategy if:
*
* * The price is not dependent on quantity or on the other contents of the Order.
* * The price calculation is based on the properties of the ProductVariant and any CustomFields
* specified on the OrderLine, for example via a product configurator.
* * The logic is a permanent part of your business requirements.
*
* Use Promotions if:
*
* * You want to implement "discounts" and "special offers"
* * The calculation is not a permanent part of your business requirements.
* * The price depends on dynamic aspects such as quantities and which other
* ProductVariants are in the Order.
* * The configuration of the logic needs to be manipulated via the Admin UI.
*
* ### Example use-cases
*
* A custom PriceCalculationStrategy can be used to implement things like:
*
* * A gift-wrapping service, where a boolean custom field is defined on the OrderLine. If `true`,
* a gift-wrapping surcharge would be added to the price.
* * A product-configurator where e.g. various finishes, colors, and materials can be selected and stored
* as OrderLine custom fields.
*
* @docsCateogory Orders
*/
export interface PriceCalculationStrategy extends InjectableStrategy {
/**
* @description
* Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
* the price for a single unit.
*/
calculateUnitPrice(
productVariant: ProductVariant,
orderLineCustomFields: { [key: string]: any },
): CalculatedPrice | Promise<CalculatedPrice>;
}
42 changes: 42 additions & 0 deletions packages/core/src/config/promotion/promotion-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,29 @@ import { PromotionUtils } from './promotion-condition';
export type PromotionActionArgType = ConfigArgSubset<'int' | 'facetValueIds'>;
export type PromotionActionArgs = ConfigArgs<PromotionActionArgType>;

/**
* @description
* The function which is used by a PromotionItemAction to calculate the
* discount on the OrderItem.
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export type ExecutePromotionItemActionFn<T extends PromotionActionArgs> = (
orderItem: OrderItem,
orderLine: OrderLine,
args: ConfigArgValues<T>,
utils: PromotionUtils,
) => number | Promise<number>;

/**
* @description
* The function which is used by a PromotionOrderAction to calculate the
* discount on the Order.
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export type ExecutePromotionOrderActionFn<T extends PromotionActionArgs> = (
order: Order,
args: ConfigArgValues<T>,
Expand All @@ -33,10 +50,32 @@ export interface PromotionActionConfig<T extends PromotionActionArgs>
extends ConfigurableOperationDefOptions<T> {
priorityValue?: number;
}

/**
* @description
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export interface PromotionItemActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
/**
* @description
* The function which contains the promotion calculation logic.
*/
execute: ExecutePromotionItemActionFn<T>;
}

/**
* @description
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export interface PromotionOrderActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
/**
* @description
* The function which contains the promotion calculation logic.
*/
execute: ExecutePromotionOrderActionFn<T>;
}

Expand All @@ -45,6 +84,7 @@ export interface PromotionOrderActionConfig<T extends PromotionActionArgs> exten
* An abstract class which is extended by {@link PromotionItemAction} and {@link PromotionOrderAction}.
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export abstract class PromotionAction<T extends PromotionActionArgs = {}> extends ConfigurableOperationDef<
T
Expand Down Expand Up @@ -75,6 +115,7 @@ export abstract class PromotionAction<T extends PromotionActionArgs = {}> extend
* ```
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export class PromotionItemAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
private readonly executeFn: ExecutePromotionItemActionFn<T>;
Expand Down Expand Up @@ -107,6 +148,7 @@ export class PromotionItemAction<T extends PromotionActionArgs = {}> extends Pro
* ```
*
* @docsCategory promotions
* @docsPage promotion-action
*/
export class PromotionOrderAction<T extends PromotionActionArgs = {}> extends PromotionAction<T> {
private readonly executeFn: ExecutePromotionOrderActionFn<T>;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/config/promotion/promotion-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type PromotionConditionArgs = ConfigArgs<PromotionConditionArgType>;
* TODO: Remove this and use the new init() method to inject providers where needed.
*
* @docsCategory promotions
* @docsPage promotion-condition
*/
export interface PromotionUtils {
/**
Expand All @@ -41,6 +42,7 @@ export interface PromotionUtils {
* A function which checks whether or not a given {@link Order} satisfies the {@link PromotionCondition}.
*
* @docsCategory promotions
* @docsPage promotion-condition
*/
export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
order: Order,
Expand All @@ -61,6 +63,7 @@ export interface PromotionConditionConfig<T extends PromotionConditionArgs>
* `true` if the Order satisfies the condition, or `false` if it does not.
*
* @docsCategory promotions
* @docsPage promotion-condition
*/
export class PromotionCondition<T extends PromotionConditionArgs = {}> extends ConfigurableOperationDef<T> {
readonly priorityValue: number;
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { EntityIdStrategy } from './entity-id-strategy/entity-id-strategy';
import { JobQueueStrategy } from './job-queue/job-queue-strategy';
import { VendureLogger } from './logger/vendure-logger';
import { OrderMergeStrategy } from './order/order-merge-strategy';
import { PriceCalculationStrategy } from './order/price-calculation-strategy';
import { PaymentMethodHandler } from './payment-method/payment-method-handler';
import { PromotionAction } from './promotion/promotion-action';
import { PromotionCondition } from './promotion/promotion-condition';
Expand Down Expand Up @@ -238,6 +239,14 @@ export interface OrderOptions {
* @default 999
*/
orderItemsLimit?: number;
/**
* @description
* Defines the logic used to calculate the unit price of an OrderItem when adding an
* item to an Order.
*
* @default DefaultPriceCalculationStrategy
*/
priceCalculationStrategy?: PriceCalculationStrategy;
/**
* @description
* Defines custom states and transition logic for the order process state machine.
Expand Down
Loading

0 comments on commit 3e2cc2b

Please sign in to comment.